feat(network): Support for editing a sonarr indexer

This commit is contained in:
2024-11-25 15:17:13 -07:00
parent 1dd4cd74c3
commit d43862a3a7
18 changed files with 1041 additions and 71 deletions
+2 -2
View File
@@ -9,9 +9,9 @@ use crate::{
cli::{mutex_flags_or_default, mutex_flags_or_option, CliCommandHandler, Command},
models::{
radarr_models::{
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
MinimumAvailability, RadarrSerdeable,
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability, RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, NetworkTrait},
+3 -2
View File
@@ -810,9 +810,10 @@ mod tests {
},
models::{
radarr_models::{
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings,
MinimumAvailability, RadarrSerdeable,
EditCollectionParams, EditMovieParams, IndexerSettings, MinimumAvailability,
RadarrSerdeable,
},
servarr_models::EditIndexerParams,
Serdeable,
},
network::{radarr_network::RadarrEvent, MockNetworkTrait, NetworkEvent},
@@ -5,7 +5,7 @@ mod tests {
use crate::event::Key;
use crate::handlers::radarr_handlers::indexers::edit_indexer_handler::EditIndexerHandler;
use crate::handlers::KeyEventHandler;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{ActiveRadarrBlock, EDIT_INDEXER_BLOCKS};
use strum::IntoEnumIterator;
@@ -14,7 +14,7 @@ mod tests {
use pretty_assertions::assert_eq;
use rstest::rstest;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
@@ -69,7 +69,7 @@ mod tests {
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use pretty_assertions::assert_eq;
use super::*;
@@ -334,7 +334,7 @@ mod tests {
use std::sync::atomic::Ordering;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
@@ -759,7 +759,7 @@ mod tests {
use rstest::rstest;
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::{
servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS, BlockSelectionState,
};
@@ -1224,7 +1224,7 @@ mod tests {
use super::*;
use crate::app::App;
use crate::event::Key;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use pretty_assertions::assert_eq;
use rstest::rstest;
@@ -1281,7 +1281,7 @@ mod tests {
mod test_handle_key_char {
use crate::app::App;
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::EDIT_INDEXER_TORRENT_SELECTION_BLOCKS;
use crate::models::BlockSelectionState;
use crate::network::radarr_network::RadarrEvent;
@@ -237,7 +237,7 @@ mod tests {
}
mod test_handle_submit {
use crate::models::servarr_data::radarr::modals::EditIndexerModal;
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::{
RadarrData, EDIT_INDEXER_NZB_SELECTION_BLOCKS, EDIT_INDEXER_TORRENT_SELECTION_BLOCKS,
};
-16
View File
@@ -179,22 +179,6 @@ pub struct EditCollectionParams {
pub search_on_add: Option<bool>,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditIndexerParams {
pub indexer_id: i64,
pub name: Option<String>,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: Option<String>,
pub api_key: Option<String>,
pub seed_ratio: Option<String>,
pub tags: Option<Vec<i64>>,
pub priority: Option<i64>,
pub clear_tags: bool,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditMovieParams {
+12
View File
@@ -1,5 +1,17 @@
use crate::models::HorizontallyScrollableText;
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EditIndexerModal {
pub name: HorizontallyScrollableText,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: HorizontallyScrollableText,
pub api_key: HorizontallyScrollableText,
pub seed_ratio: HorizontallyScrollableText,
pub tags: HorizontallyScrollableText,
}
#[derive(Default, Clone, Eq, PartialEq, Debug)]
pub struct IndexerTestResultModalItem {
pub name: String,
+1 -12
View File
@@ -3,6 +3,7 @@ use strum::IntoEnumIterator;
use crate::models::radarr_models::{
Collection, Credit, MinimumAvailability, Movie, MovieHistoryItem, MovieMonitor, RadarrRelease,
};
use crate::models::servarr_data::modals::EditIndexerModal;
use crate::models::servarr_data::radarr::radarr_data::RadarrData;
use crate::models::servarr_models::{Indexer, RootFolder};
use crate::models::stateful_list::StatefulList;
@@ -25,18 +26,6 @@ pub struct MovieDetailsModal {
pub movie_releases: StatefulTable<RadarrRelease>,
}
#[derive(Default, Debug, PartialEq, Eq)]
pub struct EditIndexerModal {
pub name: HorizontallyScrollableText,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: HorizontallyScrollableText,
pub api_key: HorizontallyScrollableText,
pub seed_ratio: HorizontallyScrollableText,
pub tags: HorizontallyScrollableText,
}
impl From<&RadarrData<'_>> for EditIndexerModal {
fn from(radarr_data: &RadarrData<'_>) -> EditIndexerModal {
let mut edit_indexer_modal = EditIndexerModal::default();
@@ -16,8 +16,6 @@ mod test {
#[rstest]
fn test_edit_indexer_modal_from_radarr_data(#[values(true, false)] seed_ratio_present: bool) {
use crate::models::servarr_models::{Indexer, IndexerField};
let mut radarr_data = RadarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..RadarrData::default()
@@ -9,9 +9,9 @@ use crate::models::radarr_models::{
AddMovieSearchResult, BlocklistItem, Collection, CollectionMovie, DownloadRecord,
IndexerSettings, Movie, RadarrTask,
};
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, MovieDetailsModal,
AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal,
};
use crate::models::servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder};
use crate::models::stateful_list::StatefulList;
+80 -1
View File
@@ -1,7 +1,8 @@
use strum::IntoEnumIterator;
use crate::models::{
servarr_models::RootFolder,
servarr_data::modals::EditIndexerModal,
servarr_models::{Indexer, RootFolder},
sonarr_models::{Episode, SeriesMonitor, SeriesType, SonarrHistoryItem, SonarrRelease},
stateful_list::StatefulList,
stateful_table::StatefulTable,
@@ -63,6 +64,84 @@ impl From<&SonarrData> for AddSeriesModal {
}
}
impl From<&SonarrData> for EditIndexerModal {
fn from(sonarr_data: &SonarrData) -> EditIndexerModal {
let mut edit_indexer_modal = EditIndexerModal::default();
let Indexer {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
tags,
fields,
..
} = sonarr_data.indexers.current_selection();
let seed_ratio_field_option = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "seedCriteria.seedRatio");
let seed_ratio_value_option = if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field.value.clone()
} else {
None
};
edit_indexer_modal.name = name.clone().unwrap().into();
edit_indexer_modal.enable_rss = Some(*enable_rss);
edit_indexer_modal.enable_automatic_search = Some(*enable_automatic_search);
edit_indexer_modal.enable_interactive_search = Some(*enable_interactive_search);
edit_indexer_modal.url = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "baseUrl")
.unwrap()
.value
.clone()
.unwrap()
.as_str()
.unwrap()
.into();
edit_indexer_modal.api_key = fields
.as_ref()
.unwrap()
.iter()
.find(|field| field.name.as_ref().unwrap() == "apiKey")
.unwrap()
.value
.clone()
.unwrap()
.as_str()
.unwrap()
.into();
if seed_ratio_value_option.is_some() {
edit_indexer_modal.seed_ratio = seed_ratio_value_option
.unwrap()
.as_f64()
.unwrap()
.to_string()
.into();
}
edit_indexer_modal.tags = tags
.iter()
.map(|tag_id| {
sonarr_data
.tags_map
.get_by_left(&tag_id.as_i64().unwrap())
.unwrap()
.clone()
})
.collect::<Vec<String>>()
.join(", ")
.into();
edit_indexer_modal
}
}
#[derive(Default)]
pub struct EpisodeDetailsModal {
pub episode_details: ScrollableText,
@@ -1,13 +1,19 @@
#[cfg(test)]
mod tests {
use bimap::BiMap;
use pretty_assertions::{assert_eq, assert_str_eq};
use rstest::rstest;
use strum::IntoEnumIterator;
use crate::models::servarr_models::{Indexer, IndexerField};
use crate::models::{
servarr_data::sonarr::{modals::AddSeriesModal, sonarr_data::SonarrData},
servarr_models::RootFolder,
sonarr_models::{SeriesMonitor, SeriesType},
};
use serde_json::{Number, Value};
use crate::models::servarr_data::modals::EditIndexerModal;
#[test]
fn test_add_series_modal_from_sonarr_data() {
@@ -55,4 +61,55 @@ mod tests {
assert!(add_series_modal.tags.text.is_empty());
assert!(add_series_modal.use_season_folder);
}
#[rstest]
fn test_edit_indexer_modal_from_sonarr_data(#[values(true, false)] seed_ratio_present: bool) {
let mut sonarr_data = SonarrData {
tags_map: BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]),
..SonarrData::default()
};
let mut fields = vec![
IndexerField {
name: Some("baseUrl".to_owned()),
value: Some(Value::String("https://test.com".to_owned())),
},
IndexerField {
name: Some("apiKey".to_owned()),
value: Some(Value::String("1234".to_owned())),
},
];
if seed_ratio_present {
fields.push(IndexerField {
name: Some("seedCriteria.seedRatio".to_owned()),
value: Some(Value::from(1.2f64)),
});
}
let indexer = Indexer {
name: Some("Test".to_owned()),
enable_rss: true,
enable_automatic_search: true,
enable_interactive_search: true,
tags: vec![Number::from(1), Number::from(2)],
fields: Some(fields),
..Indexer::default()
};
sonarr_data.indexers.set_items(vec![indexer]);
let edit_indexer_modal = EditIndexerModal::from(&sonarr_data);
assert_str_eq!(edit_indexer_modal.name.text, "Test");
assert_eq!(edit_indexer_modal.enable_rss, Some(true));
assert_eq!(edit_indexer_modal.enable_automatic_search, Some(true));
assert_eq!(edit_indexer_modal.enable_interactive_search, Some(true));
assert_str_eq!(edit_indexer_modal.url.text, "https://test.com");
assert_str_eq!(edit_indexer_modal.api_key.text, "1234");
if seed_ratio_present {
assert_str_eq!(edit_indexer_modal.seed_ratio.text, "1.2");
} else {
assert!(edit_indexer_modal.seed_ratio.text.is_empty());
}
}
}
@@ -3,7 +3,7 @@ use chrono::{DateTime, Utc};
use strum::EnumIter;
use crate::models::{
servarr_data::modals::IndexerTestResultModalItem,
servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem},
servarr_models::{DiskSpace, Indexer, QueueEvent, RootFolder},
sonarr_models::{
AddSeriesSearchResult, BlocklistItem, DownloadRecord, IndexerSettings, Season, Series,
@@ -29,6 +29,7 @@ pub struct SonarrData {
pub delete_series_files: bool,
pub downloads: StatefulTable<DownloadRecord>,
pub disk_space_vec: Vec<DiskSpace>,
pub edit_indexer_modal: Option<EditIndexerModal>,
pub edit_root_folder: Option<HorizontallyScrollableText>,
pub history: StatefulTable<SonarrHistoryItem>,
pub indexers: StatefulTable<Indexer>,
@@ -69,6 +70,7 @@ impl Default for SonarrData {
downloads: StatefulTable::default(),
delete_series_files: false,
disk_space_vec: Vec::new(),
edit_indexer_modal: None,
edit_root_folder: None,
history: StatefulTable::default(),
indexers: StatefulTable::default(),
@@ -56,6 +56,7 @@ mod tests {
assert!(!sonarr_data.delete_series_files);
assert!(sonarr_data.downloads.is_empty());
assert!(sonarr_data.disk_space_vec.is_empty());
assert!(sonarr_data.edit_indexer_modal.is_none());
assert!(sonarr_data.edit_root_folder.is_none());
assert!(sonarr_data.history.is_empty());
assert!(sonarr_data.indexers.is_empty());
+16
View File
@@ -89,6 +89,22 @@ pub struct DiskSpace {
pub total_space: i64,
}
#[derive(Default, Clone, Serialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EditIndexerParams {
pub indexer_id: i64,
pub name: Option<String>,
pub enable_rss: Option<bool>,
pub enable_automatic_search: Option<bool>,
pub enable_interactive_search: Option<bool>,
pub url: Option<String>,
pub api_key: Option<String>,
pub seed_ratio: Option<String>,
pub tags: Option<Vec<i64>>,
pub priority: Option<i64>,
pub clear_tags: bool,
}
#[derive(Default, Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct HostConfig {
+15 -11
View File
@@ -9,18 +9,18 @@ use urlencoding::encode;
use crate::models::radarr_models::{
AddMovieBody, AddMovieOptions, AddMovieSearchResult, BlocklistResponse, Collection,
CollectionMovie, Credit, CreditType, DeleteMovieParams, DownloadRecord, DownloadsResponse,
EditCollectionParams, EditIndexerParams, EditMovieParams, IndexerSettings, IndexerTestResult,
Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody,
RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus,
EditCollectionParams, EditMovieParams, IndexerSettings, IndexerTestResult, Movie,
MovieCommandBody, MovieHistoryItem, RadarrRelease, RadarrReleaseDownloadBody, RadarrSerdeable,
RadarrTask, RadarrTaskName, SystemStatus,
};
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
use crate::models::servarr_data::radarr::modals::{
AddMovieModal, EditCollectionModal, EditIndexerModal, EditMovieModal, MovieDetailsModal,
AddMovieModal, EditCollectionModal, EditMovieModal, MovieDetailsModal,
};
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::servarr_models::{
AddRootFolderBody, CommandBody, DiskSpace, HostConfig, Indexer, LogResponse, QualityProfile,
QueueEvent, RootFolder, SecurityConfig, Tag, Update,
AddRootFolderBody, CommandBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
};
use crate::models::stateful_table::StatefulTable;
use crate::models::{HorizontallyScrollableText, Route, Scrollable, ScrollableText};
@@ -188,9 +188,10 @@ impl<'a, 'b> Network<'a, 'b> {
.edit_collection(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditIndexer(params) => {
self.edit_indexer(params).await.map(RadarrSerdeable::from)
}
RadarrEvent::EditIndexer(params) => self
.edit_radarr_indexer(params)
.await
.map(RadarrSerdeable::from),
RadarrEvent::EditMovie(params) => self.edit_movie(params).await.map(RadarrSerdeable::from),
RadarrEvent::GetAllIndexerSettings => self
.get_all_radarr_indexer_settings()
@@ -882,7 +883,10 @@ impl<'a, 'b> Network<'a, 'b> {
.await
}
async fn edit_indexer(&mut self, edit_indexer_params: Option<EditIndexerParams>) -> Result<()> {
async fn edit_radarr_indexer(
&mut self,
edit_indexer_params: Option<EditIndexerParams>,
) -> Result<()> {
let detail_event = RadarrEvent::GetIndexers;
let event = RadarrEvent::EditIndexer(None);
let id = if let Some(ref params) = edit_indexer_params {
+7 -6
View File
@@ -4053,7 +4053,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event() {
async fn test_handle_edit_radarr_indexer_event() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
@@ -4153,7 +4153,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details(
async fn test_handle_edit_radarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
@@ -4255,7 +4255,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details(
async fn test_handle_edit_radarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
@@ -4371,7 +4371,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event_uses_provided_parameters() {
async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
@@ -4464,7 +4464,8 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event_uses_provided_parameters_defaults_to_previous_values() {
async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters_defaults_to_previous_values(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
@@ -4525,7 +4526,7 @@ mod test {
}
#[tokio::test]
async fn test_handle_edit_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true(
async fn test_handle_edit_radarr_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true(
) {
let indexer_details_json = json!({
"enableRss": true,
+266 -4
View File
@@ -8,15 +8,15 @@ use crate::{
models::{
radarr_models::IndexerTestResult,
servarr_data::{
modals::IndexerTestResultModalItem,
modals::{EditIndexerModal, IndexerTestResultModalItem},
sonarr::{
modals::{AddSeriesModal, EpisodeDetailsModal, SeasonDetailsModal},
sonarr_data::ActiveSonarrBlock,
},
},
servarr_models::{
AddRootFolderBody, CommandBody, DiskSpace, HostConfig, Indexer, Language, LogResponse,
QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
AddRootFolderBody, CommandBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, Language,
LogResponse, QualityProfile, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
},
sonarr_models::{
AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistResponse,
@@ -51,6 +51,7 @@ pub enum SonarrEvent {
DeleteTag(i64),
DownloadRelease(SonarrReleaseDownloadBody),
EditAllIndexerSettings(Option<IndexerSettings>),
EditIndexer(Option<EditIndexerParams>),
GetAllIndexerSettings,
GetBlocklist,
GetDownloads,
@@ -106,7 +107,9 @@ impl NetworkResource for SonarrEvent {
SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode",
SonarrEvent::GetHistory(_) | SonarrEvent::GetEpisodeHistory(_) => "/history",
SonarrEvent::GetHostConfig | SonarrEvent::GetSecurityConfig => "/config/host",
SonarrEvent::GetIndexers | SonarrEvent::DeleteIndexer(_) => "/indexer",
SonarrEvent::GetIndexers | SonarrEvent::DeleteIndexer(_) | SonarrEvent::EditIndexer(_) => {
"/indexer"
}
SonarrEvent::GetLanguageProfiles => "/languageprofile",
SonarrEvent::GetLogs(_) => "/log",
SonarrEvent::GetDiskSpace => "/diskspace",
@@ -204,6 +207,10 @@ impl<'a, 'b> Network<'a, 'b> {
.edit_all_sonarr_indexer_settings(params)
.await
.map(SonarrSerdeable::from),
SonarrEvent::EditIndexer(params) => self
.edit_sonarr_indexer(params)
.await
.map(SonarrSerdeable::from),
SonarrEvent::GetBlocklist => self.get_sonarr_blocklist().await.map(SonarrSerdeable::from),
SonarrEvent::GetDownloads => self.get_sonarr_downloads().await.map(SonarrSerdeable::from),
SonarrEvent::GetEpisodes(series_id) => self
@@ -800,6 +807,261 @@ impl<'a, 'b> Network<'a, 'b> {
resp
}
async fn edit_sonarr_indexer(
&mut self,
edit_indexer_params: Option<EditIndexerParams>,
) -> Result<()> {
let detail_event = SonarrEvent::GetIndexers;
let event = SonarrEvent::EditIndexer(None);
let id = if let Some(ref params) = edit_indexer_params {
params.indexer_id
} else {
self
.app
.lock()
.await
.data
.sonarr_data
.indexers
.current_selection()
.id
};
info!("Updating Sonarr indexer with ID: {id}");
info!("Fetching indexer details for indexer with ID: {id}");
let request_props = self
.request_props_from(
detail_event,
RequestMethod::Get,
None::<()>,
Some(format!("/{id}")),
None,
)
.await;
let mut response = String::new();
self
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
response = detailed_indexer_body.to_string()
})
.await?;
info!("Constructing edit indexer body");
let mut detailed_indexer_body: Value = serde_json::from_str(&response).unwrap();
let priority = detailed_indexer_body["priority"]
.as_i64()
.expect("Unable to deserialize 'priority'");
let (
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
) = if let Some(params) = edit_indexer_params {
let seed_ratio_field_option = detailed_indexer_body["fields"]
.as_array()
.unwrap()
.iter()
.find(|field| field["name"] == "seedCriteria.seedRatio");
let name = params.name.unwrap_or(
detailed_indexer_body["name"]
.as_str()
.expect("Unable to deserialize 'name'")
.to_owned(),
);
let enable_rss = params.enable_rss.unwrap_or(
detailed_indexer_body["enableRss"]
.as_bool()
.expect("Unable to deserialize 'enableRss'"),
);
let enable_automatic_search = params.enable_automatic_search.unwrap_or(
detailed_indexer_body["enableAutomaticSearch"]
.as_bool()
.expect("Unable to deserialize 'enableAutomaticSearch"),
);
let enable_interactive_search = params.enable_interactive_search.unwrap_or(
detailed_indexer_body["enableInteractiveSearch"]
.as_bool()
.expect("Unable to deserialize 'enableInteractiveSearch'"),
);
let url = params.url.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.expect("Unable to deserialize 'fields'")
.iter()
.find(|field| field["name"] == "baseUrl")
.expect("Field 'baseUrl' was not found in the 'fields' array")
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'baseUrl value'")
.to_owned(),
);
let api_key = params.api_key.unwrap_or(
detailed_indexer_body["fields"]
.as_array()
.expect("Unable to deserialize 'fields'")
.iter()
.find(|field| field["name"] == "apiKey")
.expect("Field 'apiKey' was not found in the 'fields' array")
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'apiKey value'")
.to_owned(),
);
let seed_ratio = params.seed_ratio.unwrap_or_else(|| {
if let Some(seed_ratio_field) = seed_ratio_field_option {
return seed_ratio_field
.get("value")
.unwrap_or(&json!(""))
.as_str()
.expect("Unable to deserialize 'seedCriteria.seedRatio value'")
.to_owned();
}
String::new()
});
let tags = if params.clear_tags {
vec![]
} else {
params.tags.unwrap_or(
detailed_indexer_body["tags"]
.as_array()
.expect("Unable to deserialize 'tags'")
.iter()
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
.collect(),
)
};
let priority = params.priority.unwrap_or(priority);
(
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
tags,
priority,
)
} else {
let tags = self
.app
.lock()
.await
.data
.sonarr_data
.edit_indexer_modal
.as_ref()
.unwrap()
.tags
.text
.clone();
let tag_ids_vec = self.extract_and_add_sonarr_tag_ids_vec(tags).await;
let mut app = self.app.lock().await;
let params = {
let EditIndexerModal {
name,
enable_rss,
enable_automatic_search,
enable_interactive_search,
url,
api_key,
seed_ratio,
..
} = app.data.sonarr_data.edit_indexer_modal.as_ref().unwrap();
(
name.text.clone(),
enable_rss.unwrap_or_default(),
enable_automatic_search.unwrap_or_default(),
enable_interactive_search.unwrap_or_default(),
url.text.clone(),
api_key.text.clone(),
seed_ratio.text.clone(),
tag_ids_vec,
priority,
)
};
app.data.sonarr_data.edit_indexer_modal = None;
params
};
*detailed_indexer_body.get_mut("name").unwrap() = json!(name);
*detailed_indexer_body.get_mut("priority").unwrap() = json!(priority);
*detailed_indexer_body.get_mut("enableRss").unwrap() = json!(enable_rss);
*detailed_indexer_body
.get_mut("enableAutomaticSearch")
.unwrap() = json!(enable_automatic_search);
*detailed_indexer_body
.get_mut("enableInteractiveSearch")
.unwrap() = json!(enable_interactive_search);
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "baseUrl")
.unwrap()
.get_mut("value")
.unwrap() = json!(url);
*detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "apiKey")
.unwrap()
.get_mut("value")
.unwrap() = json!(api_key);
*detailed_indexer_body.get_mut("tags").unwrap() = json!(tags);
let seed_ratio_field_option = detailed_indexer_body
.get_mut("fields")
.unwrap()
.as_array_mut()
.unwrap()
.iter_mut()
.find(|field| field["name"] == "seedCriteria.seedRatio");
if let Some(seed_ratio_field) = seed_ratio_field_option {
seed_ratio_field
.as_object_mut()
.unwrap()
.insert("value".to_string(), json!(seed_ratio));
}
debug!("Edit indexer body: {detailed_indexer_body:?}");
let request_props = self
.request_props_from(
event,
RequestMethod::Put,
Some(detailed_indexer_body),
Some(format!("/{id}")),
None,
)
.await;
self
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
async fn get_all_sonarr_indexer_settings(&mut self) -> Result<IndexerSettings> {
info!("Fetching Sonarr indexer settings");
let event = SonarrEvent::GetAllIndexerSettings;
+568 -4
View File
@@ -22,14 +22,14 @@ mod test {
use crate::app::{App, ServarrConfig};
use crate::models::radarr_models::IndexerTestResult;
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
use crate::models::servarr_data::modals::{EditIndexerModal, IndexerTestResultModalItem};
use crate::models::servarr_data::sonarr::modals::{
AddSeriesModal, EpisodeDetailsModal, SeasonDetailsModal,
};
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
use crate::models::servarr_models::{
DiskSpace, HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile,
QualityWrapper, QueueEvent, RootFolder, SecurityConfig, Tag, Update,
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,
@@ -205,7 +205,12 @@ mod test {
#[rstest]
fn test_resource_indexer(
#[values(SonarrEvent::GetIndexers, SonarrEvent::DeleteIndexer(None))] event: SonarrEvent,
#[values(
SonarrEvent::GetIndexers,
SonarrEvent::DeleteIndexer(None),
SonarrEvent::EditIndexer(None)
)]
event: SonarrEvent,
) {
assert_str_eq!(event.resource(), "/indexer");
}
@@ -1126,6 +1131,565 @@ mod test {
async_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.sonarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal);
app.data.sonarr_data.indexers.set_items(vec![indexer()]);
}
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(None))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.sonarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
],
"tags": [1, 2],
"id": 1
});
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.sonarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal);
let mut indexer = indexer();
indexer.fields = Some(
indexer
.fields
.unwrap()
.into_iter()
.filter(|field| field.name != Some("seedCriteria.seedRatio".to_string()))
.collect(),
);
app.data.sonarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(None))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.sonarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
{
let mut app = app_arc.lock().await;
app.data.sonarr_data.tags_map =
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
let edit_indexer_modal = EditIndexerModal {
name: "Test Update".into(),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: "https://localhost:9696/1/".into(),
api_key: "test1234".into(),
seed_ratio: "1.3".into(),
tags: "usenet, testing".into(),
};
app.data.sonarr_data.edit_indexer_modal = Some(edit_indexer_modal);
let mut indexer = indexer();
indexer.fields = Some(
indexer
.fields
.unwrap()
.into_iter()
.map(|mut field| {
if field.name == Some("seedCriteria.seedRatio".to_string()) {
field.value = None;
field
} else {
field
}
})
.collect(),
);
app.data.sonarr_data.indexers.set_items(vec![indexer]);
}
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(None))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
let app = app_arc.lock().await;
assert!(app.data.sonarr_data.edit_indexer_modal.is_none());
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters() {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let expected_indexer_edit_body_json = json!({
"enableRss": false,
"enableAutomaticSearch": false,
"enableInteractiveSearch": false,
"name": "Test Update",
"priority": 25,
"fields": [
{
"name": "baseUrl",
"value": "https://localhost:9696/1/",
},
{
"name": "apiKey",
"value": "test1234",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.3",
},
],
"tags": [1, 2],
"id": 1
});
let edit_indexer_params = EditIndexerParams {
indexer_id: 1,
name: Some("Test Update".to_owned()),
enable_rss: Some(false),
enable_automatic_search: Some(false),
enable_interactive_search: Some(false),
url: Some("https://localhost:9696/1/".to_owned()),
api_key: Some("test1234".to_owned()),
seed_ratio: Some("1.3".to_owned()),
tags: Some(vec![1, 2]),
priority: Some(25),
..EditIndexerParams::default()
};
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_indexer_edit_body_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters_defaults_to_previous_values(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1],
"id": 1
});
let edit_indexer_params = EditIndexerParams {
indexer_id: 1,
..EditIndexerParams::default()
};
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json.clone()),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(indexer_details_json))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
}
#[tokio::test]
async fn test_handle_edit_sonarr_indexer_event_uses_provided_parameters_clears_tags_when_clear_tags_is_true(
) {
let indexer_details_json = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [1, 2],
"id": 1
});
let expected_edit_indexer_body = json!({
"enableRss": true,
"enableAutomaticSearch": true,
"enableInteractiveSearch": true,
"name": "Test Indexer",
"priority": 1,
"fields": [
{
"name": "baseUrl",
"value": "https://test.com",
},
{
"name": "apiKey",
"value": "",
},
{
"name": "seedCriteria.seedRatio",
"value": "1.2",
},
],
"tags": [],
"id": 1
});
let edit_indexer_params = EditIndexerParams {
indexer_id: 1,
clear_tags: true,
..EditIndexerParams::default()
};
let (async_details_server, app_arc, mut server) = mock_servarr_api(
RequestMethod::Get,
None,
Some(indexer_details_json),
None,
SonarrEvent::GetIndexers,
Some("/1"),
None,
)
.await;
let async_edit_server = server
.mock(
"PUT",
format!("/api/v3{}/1", SonarrEvent::EditIndexer(None).resource()).as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.match_body(Matcher::Json(expected_edit_indexer_body))
.create_async()
.await;
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
assert!(network
.handle_sonarr_event(SonarrEvent::EditIndexer(Some(edit_indexer_params)))
.await
.is_ok());
async_details_server.assert_async().await;
async_edit_server.assert_async().await;
}
#[rstest]
#[tokio::test]
async fn test_handle_get_sonarr_blocklist_event(#[values(true, false)] use_custom_sorting: bool) {