use crate::models::Route; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesSearchResult, DeleteSeriesParams, EditSeriesParams, Series, SonarrCommandBody, SonarrHistoryItem, }; use crate::models::stateful_table::StatefulTable; use crate::network::sonarr_network::SonarrEvent; use crate::network::{Network, RequestMethod}; use anyhow::Result; use log::{debug, info, warn}; use serde_json::{Value, json}; use urlencoding::encode; #[cfg(test)] #[path = "sonarr_series_network_tests.rs"] mod sonarr_series_network_tests; impl Network<'_, '_> { pub(in crate::network::sonarr_network) async fn add_sonarr_series( &mut self, mut add_series_body: AddSeriesBody, ) -> anyhow::Result { info!("Adding new series to Sonarr"); let event = SonarrEvent::AddSeries(AddSeriesBody::default()); if let Some(tag_input_str) = add_series_body.tag_input_string.as_ref() { let tag_ids_vec = self.extract_and_add_sonarr_tag_ids_vec(tag_input_str).await; add_series_body.tags = tag_ids_vec; } debug!("Add series body: {add_series_body:?}"); let request_props = self .request_props_from( event, RequestMethod::Post, Some(add_series_body), None, None, ) .await; self .handle_request::(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn delete_series( &mut self, delete_series_params: DeleteSeriesParams, ) -> Result<()> { let event = SonarrEvent::DeleteSeries(DeleteSeriesParams::default()); let DeleteSeriesParams { id, delete_series_files, add_list_exclusion, } = delete_series_params; info!( "Deleting Sonarr series with ID: {id} with deleteFiles={delete_series_files} and addImportExclusion={add_list_exclusion}" ); let request_props = self .request_props_from( event, RequestMethod::Delete, None::<()>, Some(format!("/{id}")), Some(format!( "deleteFiles={delete_series_files}&addImportExclusion={add_list_exclusion}" )), ) .await; self .handle_request::<(), ()>(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn edit_sonarr_series( &mut self, mut edit_series_params: EditSeriesParams, ) -> Result<()> { info!("Editing Sonarr series"); if let Some(tag_input_str) = edit_series_params.tag_input_string.as_ref() { let tag_ids_vec = self.extract_and_add_sonarr_tag_ids_vec(tag_input_str).await; edit_series_params.tags = Some(tag_ids_vec); } let series_id = edit_series_params.series_id; let detail_event = SonarrEvent::GetSeriesDetails(series_id); let event = SonarrEvent::EditSeries(EditSeriesParams::default()); info!("Fetching series details for series with ID: {series_id}"); let request_props = self .request_props_from( detail_event, RequestMethod::Get, None::<()>, Some(format!("/{series_id}")), None, ) .await; let mut response = String::new(); self .handle_request::<(), Value>(request_props, |detailed_series_body, _| { response = detailed_series_body.to_string() }) .await?; info!("Constructing edit series body"); let mut detailed_series_body: Value = serde_json::from_str(&response)?; let ( monitored, use_season_folders, series_type, quality_profile_id, language_profile_id, root_folder_path, tags, ) = { let monitored = edit_series_params.monitored.unwrap_or( detailed_series_body["monitored"] .as_bool() .expect("Unable to deserialize 'monitored'"), ); let use_season_folders = edit_series_params.use_season_folders.unwrap_or( detailed_series_body["seasonFolder"] .as_bool() .expect("Unable to deserialize 'season_folder'"), ); let series_type = edit_series_params .series_type .unwrap_or_else(|| { serde_json::from_value(detailed_series_body["seriesType"].clone()) .expect("Unable to deserialize 'seriesType'") }) .to_string(); let quality_profile_id = edit_series_params.quality_profile_id.unwrap_or_else(|| { detailed_series_body["qualityProfileId"] .as_i64() .expect("Unable to deserialize 'qualityProfileId'") }); let language_profile_id = edit_series_params.language_profile_id.unwrap_or_else(|| { detailed_series_body["languageProfileId"] .as_i64() .expect("Unable to deserialize 'languageProfileId'") }); let root_folder_path = edit_series_params.root_folder_path.unwrap_or_else(|| { detailed_series_body["path"] .as_str() .expect("Unable to deserialize 'path'") .to_owned() }); let tags = if edit_series_params.clear_tags { vec![] } else { edit_series_params.tags.unwrap_or( detailed_series_body["tags"] .as_array() .expect("Unable to deserialize 'tags'") .iter() .map(|item| item.as_i64().expect("Unable to deserialize tag ID")) .collect(), ) }; ( monitored, use_season_folders, series_type, quality_profile_id, language_profile_id, root_folder_path, tags, ) }; *detailed_series_body.get_mut("monitored").unwrap() = json!(monitored); *detailed_series_body.get_mut("seasonFolder").unwrap() = json!(use_season_folders); *detailed_series_body.get_mut("seriesType").unwrap() = json!(series_type); *detailed_series_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id); *detailed_series_body.get_mut("languageProfileId").unwrap() = json!(language_profile_id); *detailed_series_body.get_mut("path").unwrap() = json!(root_folder_path); *detailed_series_body.get_mut("tags").unwrap() = json!(tags); debug!("Edit series body: {detailed_series_body:?}"); let request_props = self .request_props_from( event, RequestMethod::Put, Some(detailed_series_body), Some(format!("/{series_id}")), None, ) .await; self .handle_request::(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn toggle_sonarr_series_monitoring( &mut self, series_id: i64, ) -> Result<()> { let event = SonarrEvent::ToggleSeriesMonitoring(series_id); let detail_event = SonarrEvent::GetSeriesDetails(series_id); info!("Toggling series monitoring for series with ID: {series_id}"); info!("Fetching series details for series with ID: {series_id}"); let request_props = self .request_props_from( detail_event, RequestMethod::Get, None::<()>, Some(format!("/{series_id}")), None, ) .await; let mut response = String::new(); self .handle_request::<(), Value>(request_props, |detailed_series_body, _| { response = detailed_series_body.to_string() }) .await?; info!("Constructing toggle series monitoring body"); match serde_json::from_str::(&response) { Ok(mut detailed_series_body) => { let monitored = detailed_series_body .get("monitored") .unwrap() .as_bool() .unwrap(); *detailed_series_body.get_mut("monitored").unwrap() = json!(!monitored); debug!("Toggle series monitoring body: {detailed_series_body:?}"); let request_props = self .request_props_from( event, RequestMethod::Put, Some(detailed_series_body), Some(format!("/{series_id}")), None, ) .await; self .handle_request::(request_props, |_, _| ()) .await } Err(_) => { warn!("Request for detailed series body was interrupted"); Ok(()) } } } pub(in crate::network::sonarr_network) async fn get_series_details( &mut self, series_id: i64, ) -> Result { info!("Fetching details for Sonarr series with ID: {series_id}"); let event = SonarrEvent::GetSeriesDetails(series_id); let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, Some(format!("/{series_id}")), None, ) .await; self .handle_request::<(), Series>(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn get_sonarr_series_history( &mut self, series_id: i64, ) -> Result> { info!("Fetching Sonarr series history for series with ID: {series_id}"); let event = SonarrEvent::GetSeriesHistory(series_id); let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, None, Some(format!("seriesId={series_id}")), ) .await; self .handle_request::<(), Vec>(request_props, |mut history_vec, mut app| { if app.data.sonarr_data.series_history.is_none() { app.data.sonarr_data.series_history = Some(StatefulTable::default()); } if !matches!( app.get_current_route(), Route::Sonarr(ActiveSonarrBlock::SeriesHistorySortPrompt, _) ) { history_vec.sort_by(|a, b| a.id.cmp(&b.id)); app .data .sonarr_data .series_history .as_mut() .unwrap() .set_items(history_vec); app .data .sonarr_data .series_history .as_mut() .unwrap() .apply_sorting_toggle(false); } }) .await } pub(in crate::network::sonarr_network) async fn list_series(&mut self) -> Result> { info!("Fetching Sonarr library"); let event = SonarrEvent::ListSeries; let request_props = self .request_props_from(event, RequestMethod::Get, None::<()>, None, None) .await; self .handle_request::<(), Vec>(request_props, |mut series_vec, mut app| { if !matches!( app.get_current_route(), Route::Sonarr(ActiveSonarrBlock::SeriesSortPrompt, _) ) { series_vec.sort_by(|a, b| a.id.cmp(&b.id)); app.data.sonarr_data.series.set_items(series_vec); app.data.sonarr_data.series.apply_sorting_toggle(false); } }) .await } pub(in crate::network::sonarr_network) async fn search_sonarr_series( &mut self, query: String, ) -> Result> { info!("Searching for specific Sonarr series"); let event = SonarrEvent::SearchNewSeries(String::new()); let request_props = self .request_props_from( event, RequestMethod::Get, None::<()>, None, Some(format!("term={}", encode(&query))), ) .await; self .handle_request::<(), Vec>(request_props, |series_vec, mut app| { if series_vec.is_empty() { app.pop_and_push_navigation_stack(ActiveSonarrBlock::AddSeriesEmptySearchResults.into()); } else if let Some(add_searched_seriess) = app.data.sonarr_data.add_searched_series.as_mut() { add_searched_seriess.set_items(series_vec); } else { let mut add_searched_seriess = StatefulTable::default(); add_searched_seriess.set_items(series_vec); app.data.sonarr_data.add_searched_series = Some(add_searched_seriess); } }) .await } pub(in crate::network::sonarr_network) async fn trigger_automatic_series_search( &mut self, series_id: i64, ) -> Result { let event = SonarrEvent::TriggerAutomaticSeriesSearch(series_id); info!("Searching indexers for series with ID: {series_id}"); let body = SonarrCommandBody { name: "SeriesSearch".to_owned(), series_id: Some(series_id), ..SonarrCommandBody::default() }; let request_props = self .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self .handle_request::(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn update_all_series(&mut self) -> Result { info!("Updating all series"); let event = SonarrEvent::UpdateAllSeries; let body = SonarrCommandBody { name: "RefreshSeries".to_owned(), ..SonarrCommandBody::default() }; let request_props = self .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self .handle_request::(request_props, |_, _| ()) .await } pub(in crate::network::sonarr_network) async fn update_and_scan_series( &mut self, series_id: i64, ) -> Result { let event = SonarrEvent::UpdateAndScanSeries(series_id); info!("Updating and scanning series with ID: {series_id}"); let body = SonarrCommandBody { name: "RefreshSeries".to_owned(), series_id: Some(series_id), ..SonarrCommandBody::default() }; let request_props = self .request_props_from(event, RequestMethod::Post, Some(body), None, None) .await; self .handle_request::(request_props, |_, _| ()) .await } }