refactor: Network module is now broken out into similar directory structures for each servarr to mimic the rest of the project to make it easier to develop and maintain
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
use crate::models::radarr_models::BlocklistResponse;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::Route;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_blocklist_network_tests.rs"]
|
||||
mod radarr_blocklist_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn clear_radarr_blocklist(&mut self) -> Result<()> {
|
||||
info!("Clearing Radarr blocklist");
|
||||
let event = RadarrEvent::ClearBlocklist;
|
||||
|
||||
let ids = self
|
||||
.app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.blocklist
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.id)
|
||||
.collect::<Vec<i64>>();
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
Some(json!({"ids": ids})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn delete_radarr_blocklist_item(
|
||||
&mut self,
|
||||
blocklist_item_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = RadarrEvent::DeleteBlocklistItem(blocklist_item_id);
|
||||
|
||||
info!("Deleting Radarr blocklist item for item with id: {blocklist_item_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{blocklist_item_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_blocklist(
|
||||
&mut self,
|
||||
) -> Result<BlocklistResponse> {
|
||||
info!("Fetching Radarr blocklist");
|
||||
let event = RadarrEvent::GetBlocklist;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Radarr(ActiveRadarrBlock::BlocklistSortPrompt, _)
|
||||
) {
|
||||
let mut blocklist_vec = blocklist_resp.records;
|
||||
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.radarr_data.blocklist.set_items(blocklist_vec);
|
||||
app.data.radarr_data.blocklist.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::BlocklistItem;
|
||||
use crate::models::radarr_models::BlocklistItemMovie;
|
||||
use crate::models::radarr_models::BlocklistResponse;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::radarr_network_test_utils::test_utils::blocklist_item;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::radarr_network::RadarrSerdeable;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_radarr_blocklist_event() {
|
||||
let blocklist_items = vec![
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 2,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 3,
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let expected_request_json = json!({ "ids": [1, 2, 3]});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
Some(expected_request_json),
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::ClearBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.blocklist
|
||||
.set_items(blocklist_items);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::ClearBlocklist)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_radarr_blocklist_item_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::DeleteBlocklistItem(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::DeleteBlocklistItem(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_blocklist_event(#[values(true, false)] use_custom_sorting: bool) {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"id": 123,
|
||||
"movieId": 1007,
|
||||
"sourceTitle": "z movie",
|
||||
"languages": [{"id": 1, "name": "English"}],
|
||||
"quality": {"quality": {"name": "HD - 1080p"}},
|
||||
"customFormats": [{"id": 1, "name": "English"}],
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "DrunkenSlug (Prowlarr)",
|
||||
"message": "test message",
|
||||
"movie": {
|
||||
"id": 1007,
|
||||
"title": "z movie",
|
||||
"tmdbId": 1234,
|
||||
"originalLanguage": {"id": 1, "name": "English"},
|
||||
"sizeOnDisk": 3543348019i64,
|
||||
"status": "Downloaded",
|
||||
"overview": "Blah blah blah",
|
||||
"path": "/nfs/movies",
|
||||
"studio": "21st Century Alex",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"year": 2023,
|
||||
"monitored": true,
|
||||
"hasFile": true,
|
||||
"runtime": 120,
|
||||
"qualityProfileId": 2222,
|
||||
"minimumAvailability": "announced",
|
||||
"certification": "R",
|
||||
"tags": [1],
|
||||
"ratings": {
|
||||
"imdb": {"value": 9.9},
|
||||
"tmdb": {"value": 9.9},
|
||||
"rottenTomatoes": {"value": 9.9}
|
||||
},
|
||||
},
|
||||
}, {
|
||||
"id": 456,
|
||||
"movieId": 2001,
|
||||
"sourceTitle": "A Movie",
|
||||
"languages": [{"id": 1, "name": "English"}],
|
||||
"quality": {"quality": {"name": "HD - 1080p"}},
|
||||
"customFormats": [{"id": 1, "name": "English"}],
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "DrunkenSlug (Prowlarr)",
|
||||
"message": "test message",
|
||||
"movie": {
|
||||
"id": 2001,
|
||||
"title": "A Movie",
|
||||
"tmdbId": 1234,
|
||||
"originalLanguage": {"id": 1, "name": "English"},
|
||||
"sizeOnDisk": 3543348019i64,
|
||||
"status": "Downloaded",
|
||||
"overview": "Blah blah blah",
|
||||
"path": "/nfs/movies",
|
||||
"studio": "21st Century Alex",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"year": 2023,
|
||||
"monitored": true,
|
||||
"hasFile": true,
|
||||
"runtime": 120,
|
||||
"qualityProfileId": 2222,
|
||||
"minimumAvailability": "announced",
|
||||
"certification": "R",
|
||||
"tags": [1],
|
||||
"ratings": {
|
||||
"imdb": {"value": 9.9},
|
||||
"tmdb": {"value": 9.9},
|
||||
"rottenTomatoes": {"value": 9.9}
|
||||
},
|
||||
},
|
||||
}]});
|
||||
let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap();
|
||||
let mut expected_blocklist = vec![
|
||||
BlocklistItem {
|
||||
id: 123,
|
||||
movie_id: 1007,
|
||||
source_title: "z movie".into(),
|
||||
movie: BlocklistItemMovie {
|
||||
title: "z movie".into(),
|
||||
},
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 456,
|
||||
movie_id: 2001,
|
||||
source_title: "A Movie".into(),
|
||||
movie: BlocklistItemMovie {
|
||||
title: "A Movie".into(),
|
||||
},
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(blocklist_json),
|
||||
None,
|
||||
RadarrEvent::GetBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true;
|
||||
if use_custom_sorting {
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
expected_blocklist.sort_by(cmp_fn);
|
||||
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
}
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::BlocklistResponse(blocklist) = network
|
||||
.handle_radarr_event(RadarrEvent::GetBlocklist)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.blocklist.items,
|
||||
expected_blocklist
|
||||
);
|
||||
assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc);
|
||||
assert_eq!(blocklist, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_blocklist_event_no_op_when_user_is_selecting_sort_options() {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"id": 123,
|
||||
"movieId": 1007,
|
||||
"sourceTitle": "z movie",
|
||||
"languages": [{"id": 1, "name": "English"}],
|
||||
"quality": {"quality": {"name": "HD - 1080p"}},
|
||||
"customFormats": [{"id": 1, "name": "English"}],
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "DrunkenSlug (Prowlarr)",
|
||||
"message": "test message",
|
||||
"movie": {
|
||||
"id": 1007,
|
||||
"title": "z movie",
|
||||
"tmdbId": 1234,
|
||||
"originalLanguage": {"id": 1, "name": "English"},
|
||||
"sizeOnDisk": 3543348019i64,
|
||||
"status": "Downloaded",
|
||||
"overview": "Blah blah blah",
|
||||
"path": "/nfs/movies",
|
||||
"studio": "21st Century Alex",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"year": 2023,
|
||||
"monitored": true,
|
||||
"hasFile": true,
|
||||
"runtime": 120,
|
||||
"qualityProfileId": 2222,
|
||||
"minimumAvailability": "announced",
|
||||
"certification": "R",
|
||||
"tags": [1],
|
||||
"ratings": {
|
||||
"imdb": {"value": 9.9},
|
||||
"tmdb": {"value": 9.9},
|
||||
"rottenTomatoes": {"value": 9.9}
|
||||
},
|
||||
},
|
||||
}, {
|
||||
"id": 456,
|
||||
"movieId": 2001,
|
||||
"sourceTitle": "A Movie",
|
||||
"languages": [{"id": 1, "name": "English"}],
|
||||
"quality": {"quality": {"name": "HD - 1080p"}},
|
||||
"customFormats": [{"id": 1, "name": "English"}],
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "DrunkenSlug (Prowlarr)",
|
||||
"message": "test message",
|
||||
"movie": {
|
||||
"id": 2001,
|
||||
"title": "A Movie",
|
||||
"tmdbId": 1234,
|
||||
"originalLanguage": {"id": 1, "name": "English"},
|
||||
"sizeOnDisk": 3543348019i64,
|
||||
"status": "Downloaded",
|
||||
"overview": "Blah blah blah",
|
||||
"path": "/nfs/movies",
|
||||
"studio": "21st Century Alex",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"year": 2023,
|
||||
"monitored": true,
|
||||
"hasFile": true,
|
||||
"runtime": 120,
|
||||
"qualityProfileId": 2222,
|
||||
"minimumAvailability": "announced",
|
||||
"certification": "R",
|
||||
"tags": [1],
|
||||
"ratings": {
|
||||
"imdb": {"value": 9.9},
|
||||
"tmdb": {"value": 9.9},
|
||||
"rottenTomatoes": {"value": 9.9}
|
||||
},
|
||||
},
|
||||
}]});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(blocklist_json),
|
||||
None,
|
||||
RadarrEvent::GetBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.blocklist.sort_asc = true;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.push_navigation_stack(ActiveRadarrBlock::BlocklistSortPrompt.into());
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::GetBlocklist)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.blocklist
|
||||
.items
|
||||
.is_empty());
|
||||
assert!(app_arc.lock().await.data.radarr_data.blocklist.sort_asc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use crate::models::radarr_models::{Collection, EditCollectionParams};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::servarr_models::CommandBody;
|
||||
use crate::models::Route;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_collections_network_tests.rs"]
|
||||
mod radarr_collections_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn edit_collection(
|
||||
&mut self,
|
||||
edit_collection_params: EditCollectionParams,
|
||||
) -> Result<()> {
|
||||
info!("Editing Radarr collection");
|
||||
let detail_event = RadarrEvent::GetCollections;
|
||||
let event = RadarrEvent::EditCollection(EditCollectionParams::default());
|
||||
info!("Fetching collection details");
|
||||
let collection_id = edit_collection_params.collection_id;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{collection_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_collection_body, _| {
|
||||
response = detailed_collection_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing edit collection body");
|
||||
|
||||
let mut detailed_collection_body: Value = serde_json::from_str(&response)?;
|
||||
let (monitored, minimum_availability, quality_profile_id, root_folder_path, search_on_add) = {
|
||||
let monitored = edit_collection_params.monitored.unwrap_or_else(|| {
|
||||
detailed_collection_body["monitored"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'monitored' bool")
|
||||
});
|
||||
let minimum_availability = edit_collection_params
|
||||
.minimum_availability
|
||||
.unwrap_or_else(|| {
|
||||
serde_json::from_value(detailed_collection_body["minimumAvailability"].clone())
|
||||
.expect("Unable to deserialize 'minimumAvailability'")
|
||||
})
|
||||
.to_string();
|
||||
let quality_profile_id = edit_collection_params
|
||||
.quality_profile_id
|
||||
.unwrap_or_else(|| {
|
||||
detailed_collection_body["qualityProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'qualityProfileId'")
|
||||
});
|
||||
let root_folder_path = edit_collection_params.root_folder_path.unwrap_or_else(|| {
|
||||
detailed_collection_body["rootFolderPath"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'rootFolderPath'")
|
||||
.to_owned()
|
||||
});
|
||||
let search_on_add = edit_collection_params.search_on_add.unwrap_or_else(|| {
|
||||
detailed_collection_body["searchOnAdd"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'searchOnAdd'")
|
||||
});
|
||||
|
||||
(
|
||||
monitored,
|
||||
minimum_availability,
|
||||
quality_profile_id,
|
||||
root_folder_path,
|
||||
search_on_add,
|
||||
)
|
||||
};
|
||||
|
||||
*detailed_collection_body.get_mut("monitored").unwrap() = json!(monitored);
|
||||
*detailed_collection_body
|
||||
.get_mut("minimumAvailability")
|
||||
.unwrap() = json!(minimum_availability);
|
||||
*detailed_collection_body
|
||||
.get_mut("qualityProfileId")
|
||||
.unwrap() = json!(quality_profile_id);
|
||||
*detailed_collection_body.get_mut("rootFolderPath").unwrap() = json!(root_folder_path);
|
||||
*detailed_collection_body.get_mut("searchOnAdd").unwrap() = json!(search_on_add);
|
||||
|
||||
debug!("Edit collection body: {detailed_collection_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_collection_body),
|
||||
Some(format!("/{collection_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_collections(
|
||||
&mut self,
|
||||
) -> Result<Vec<Collection>> {
|
||||
info!("Fetching Radarr collections");
|
||||
let event = RadarrEvent::GetCollections;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Collection>>(request_props, |mut collections_vec, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Radarr(ActiveRadarrBlock::CollectionsSortPrompt, _)
|
||||
) {
|
||||
collections_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.radarr_data.collections.set_items(collections_vec);
|
||||
app.data.radarr_data.collections.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn update_collections(&mut self) -> Result<Value> {
|
||||
info!("Updating collections");
|
||||
let event = RadarrEvent::UpdateCollections;
|
||||
let body = CommandBody {
|
||||
name: "RefreshCollections".to_owned(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::{
|
||||
Collection, EditCollectionParams, MinimumAvailability, RadarrSerdeable,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::radarr_network_test_utils::test_utils::collection;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, NetworkResource, RequestMethod};
|
||||
use mockito::Matcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_collection_event() {
|
||||
let detailed_collection_body = json!({
|
||||
"id": 123,
|
||||
"title": "Test Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [
|
||||
{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
let mut expected_body = detailed_collection_body.clone();
|
||||
*expected_body.get_mut("monitored").unwrap() = json!(false);
|
||||
*expected_body.get_mut("minimumAvailability").unwrap() = json!("announced");
|
||||
*expected_body.get_mut("qualityProfileId").unwrap() = json!(1111);
|
||||
*expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/Test Path");
|
||||
*expected_body.get_mut("searchOnAdd").unwrap() = json!(false);
|
||||
let edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
monitored: Some(false),
|
||||
minimum_availability: Some(MinimumAvailability::Announced),
|
||||
quality_profile_id: Some(1111),
|
||||
root_folder_path: Some("/nfs/Test Path".to_owned()),
|
||||
search_on_add: Some(false),
|
||||
};
|
||||
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(detailed_collection_body),
|
||||
None,
|
||||
RadarrEvent::GetCollections,
|
||||
Some("/123"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/123",
|
||||
RadarrEvent::EditCollection(edit_collection_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_body))
|
||||
.create_async()
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditCollection(edit_collection_params))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_details_server.assert_async().await;
|
||||
async_edit_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_collection_event_defaults_to_previous_values_when_no_params_are_provided(
|
||||
) {
|
||||
let detailed_collection_body = json!({
|
||||
"id": 123,
|
||||
"title": "Test Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [
|
||||
{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
let mut expected_body = detailed_collection_body.clone();
|
||||
*expected_body.get_mut("monitored").unwrap() = json!(true);
|
||||
*expected_body.get_mut("minimumAvailability").unwrap() = json!("released");
|
||||
*expected_body.get_mut("qualityProfileId").unwrap() = json!(2222);
|
||||
*expected_body.get_mut("rootFolderPath").unwrap() = json!("/nfs/movies");
|
||||
*expected_body.get_mut("searchOnAdd").unwrap() = json!(true);
|
||||
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(detailed_collection_body),
|
||||
None,
|
||||
RadarrEvent::GetCollections,
|
||||
Some("/123"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
..EditCollectionParams::default()
|
||||
};
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/123",
|
||||
RadarrEvent::EditCollection(edit_collection_params).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_body))
|
||||
.create_async()
|
||||
.await;
|
||||
let edit_collection_params = EditCollectionParams {
|
||||
collection_id: 123,
|
||||
..EditCollectionParams::default()
|
||||
};
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditCollection(edit_collection_params))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_details_server.assert_async().await;
|
||||
async_edit_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_collections_event(#[values(true, false)] use_custom_sorting: bool) {
|
||||
let collections_json = json!([{
|
||||
"id": 123,
|
||||
"title": "z Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}],
|
||||
},
|
||||
{
|
||||
"id": 456,
|
||||
"title": "A Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}],
|
||||
}]);
|
||||
let response: Vec<Collection> = serde_json::from_value(collections_json.clone()).unwrap();
|
||||
let mut expected_collections = vec![
|
||||
Collection {
|
||||
id: 123,
|
||||
title: "z Collection".into(),
|
||||
..collection()
|
||||
},
|
||||
Collection {
|
||||
id: 456,
|
||||
title: "A Collection".into(),
|
||||
..collection()
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(collections_json),
|
||||
None,
|
||||
RadarrEvent::GetCollections,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.collections.sort_asc = true;
|
||||
if use_custom_sorting {
|
||||
let cmp_fn = |a: &Collection, b: &Collection| {
|
||||
a.title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.title.text.to_lowercase())
|
||||
};
|
||||
expected_collections.sort_by(cmp_fn);
|
||||
|
||||
let collection_sort_option = SortOption {
|
||||
name: "Collection",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.sorting(vec![collection_sort_option]);
|
||||
}
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Collections(collections) = network
|
||||
.handle_radarr_event(RadarrEvent::GetCollections)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.collections.items,
|
||||
expected_collections
|
||||
);
|
||||
assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc);
|
||||
assert_eq!(collections, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_collections_event_no_op_when_user_is_selecting_sort_options() {
|
||||
let collections_json = json!([{
|
||||
"id": 123,
|
||||
"title": "z Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}],
|
||||
},
|
||||
{
|
||||
"id": 456,
|
||||
"title": "A Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}],
|
||||
}]);
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(collections_json),
|
||||
None,
|
||||
RadarrEvent::GetCollections,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.collections.sort_asc = true;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.push_navigation_stack(ActiveRadarrBlock::CollectionsSortPrompt.into());
|
||||
let cmp_fn = |a: &Collection, b: &Collection| {
|
||||
a.title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.title.text.to_lowercase())
|
||||
};
|
||||
let collection_sort_option = SortOption {
|
||||
name: "Collection",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.sorting(vec![collection_sort_option]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::GetCollections)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.collections
|
||||
.items
|
||||
.is_empty());
|
||||
assert!(app_arc.lock().await.data.radarr_data.collections.sort_asc);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_update_collections_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "RefreshCollections"
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
RadarrEvent::UpdateCollections,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::UpdateCollections)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use crate::models::radarr_models::DownloadsResponse;
|
||||
use crate::models::servarr_models::CommandBody;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_downloads_network_tests.rs"]
|
||||
mod radarr_downloads_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn delete_radarr_download(
|
||||
&mut self,
|
||||
download_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = RadarrEvent::DeleteDownload(download_id);
|
||||
info!("Deleting Radarr download for download with id: {download_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{download_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_downloads(
|
||||
&mut self,
|
||||
count: u64,
|
||||
) -> Result<DownloadsResponse> {
|
||||
info!("Fetching Radarr downloads");
|
||||
let event = RadarrEvent::GetDownloads(count);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("pageSize={count}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.downloads
|
||||
.set_items(queue_response.records);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn update_radarr_downloads(
|
||||
&mut self,
|
||||
) -> Result<Value> {
|
||||
info!("Updating Radarr downloads");
|
||||
let event = RadarrEvent::UpdateDownloads;
|
||||
let body = CommandBody {
|
||||
name: "RefreshMonitoredDownloads".to_owned(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::{DownloadsResponse, RadarrSerdeable};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::radarr_network_test_utils::test_utils::downloads_response;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_radarr_download_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::DeleteDownload(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::DeleteDownload(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_downloads_event() {
|
||||
let downloads_response_json = json!({
|
||||
"records": [{
|
||||
"title": "Test Download Title",
|
||||
"status": "downloading",
|
||||
"id": 1,
|
||||
"movieId": 1,
|
||||
"size": 3543348019u64,
|
||||
"sizeleft": 1771674009,
|
||||
"outputPath": "/nfs/movies/Test",
|
||||
"indexer": "kickass torrents",
|
||||
"downloadClient": "transmission",
|
||||
}]
|
||||
});
|
||||
let response: DownloadsResponse =
|
||||
serde_json::from_value(downloads_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(downloads_response_json),
|
||||
None,
|
||||
RadarrEvent::GetDownloads(500),
|
||||
None,
|
||||
Some("pageSize=500"),
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::DownloadsResponse(downloads) = network
|
||||
.handle_radarr_event(RadarrEvent::GetDownloads(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
pretty_assertions::assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.downloads.items,
|
||||
downloads_response().records
|
||||
);
|
||||
pretty_assertions::assert_eq!(downloads, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_update_radarr_downloads_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "RefreshMonitoredDownloads"
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
RadarrEvent::UpdateDownloads,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::UpdateDownloads)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
use crate::models::radarr_models::IndexerSettings;
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_indexers_network_tests.rs"]
|
||||
mod radarr_indexers_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn delete_radarr_indexer(
|
||||
&mut self,
|
||||
indexer_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = RadarrEvent::DeleteIndexer(indexer_id);
|
||||
info!("Deleting Radarr indexer for indexer with id: {indexer_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{indexer_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn edit_all_radarr_indexer_settings(
|
||||
&mut self,
|
||||
params: IndexerSettings,
|
||||
) -> Result<Value> {
|
||||
info!("Updating Radarr indexer settings");
|
||||
let event = RadarrEvent::EditAllIndexerSettings(IndexerSettings::default());
|
||||
|
||||
debug!("Indexer settings body: {params:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Put, Some(params), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn edit_radarr_indexer(
|
||||
&mut self,
|
||||
mut edit_indexer_params: EditIndexerParams,
|
||||
) -> Result<()> {
|
||||
let detail_event = RadarrEvent::GetIndexers;
|
||||
let event = RadarrEvent::EditIndexer(EditIndexerParams::default());
|
||||
let id = edit_indexer_params.indexer_id;
|
||||
if let Some(tag_input_str) = edit_indexer_params.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tag_input_str).await;
|
||||
edit_indexer_params.tags = Some(tag_ids_vec);
|
||||
}
|
||||
info!("Updating Radarr 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)?;
|
||||
|
||||
let (
|
||||
name,
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags,
|
||||
priority,
|
||||
) = {
|
||||
let priority = detailed_indexer_body["priority"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'priority'");
|
||||
let seed_ratio_field_option = detailed_indexer_body["fields"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|field| field["name"] == "seedCriteria.seedRatio");
|
||||
let name = edit_indexer_params.name.unwrap_or(
|
||||
detailed_indexer_body["name"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'name'")
|
||||
.to_owned(),
|
||||
);
|
||||
let enable_rss = edit_indexer_params.enable_rss.unwrap_or(
|
||||
detailed_indexer_body["enableRss"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableRss'"),
|
||||
);
|
||||
let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or(
|
||||
detailed_indexer_body["enableAutomaticSearch"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableAutomaticSearch"),
|
||||
);
|
||||
let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or(
|
||||
detailed_indexer_body["enableInteractiveSearch"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableInteractiveSearch'"),
|
||||
);
|
||||
let url = edit_indexer_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 = edit_indexer_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 = edit_indexer_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 edit_indexer_params.clear_tags {
|
||||
vec![]
|
||||
} else {
|
||||
edit_indexer_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 = edit_indexer_params.priority.unwrap_or(priority);
|
||||
|
||||
(
|
||||
name,
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags,
|
||||
priority,
|
||||
)
|
||||
};
|
||||
|
||||
*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}")),
|
||||
Some("forceSave=true".to_owned()),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_indexers(
|
||||
&mut self,
|
||||
) -> Result<Vec<Indexer>> {
|
||||
info!("Fetching Radarr indexers");
|
||||
let event = RadarrEvent::GetIndexers;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Indexer>>(request_props, |indexers, mut app| {
|
||||
app.data.radarr_data.indexers.set_items(indexers);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_all_radarr_indexer_settings(
|
||||
&mut self,
|
||||
) -> Result<IndexerSettings> {
|
||||
info!("Fetching Radarr indexer settings");
|
||||
let event = RadarrEvent::GetAllIndexerSettings;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| {
|
||||
if app.data.radarr_data.indexer_settings.is_none() {
|
||||
app.data.radarr_data.indexer_settings = Some(indexer_settings);
|
||||
} else {
|
||||
debug!("Indexer Settings are being modified. Ignoring update...");
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn test_radarr_indexer(
|
||||
&mut self,
|
||||
indexer_id: i64,
|
||||
) -> Result<Value> {
|
||||
let detail_event = RadarrEvent::GetIndexers;
|
||||
let event = RadarrEvent::TestIndexer(indexer_id);
|
||||
info!("Testing Radarr indexer with ID: {indexer_id}");
|
||||
|
||||
info!("Fetching indexer details for indexer with ID: {indexer_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{indexer_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut test_body: Value = Value::default();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
|
||||
test_body = detailed_indexer_body;
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Testing indexer");
|
||||
|
||||
let mut request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(test_body), None, None)
|
||||
.await;
|
||||
request_props.ignore_status_code = true;
|
||||
|
||||
self
|
||||
.handle_request::<Value, Value>(request_props, |test_results, mut app| {
|
||||
if test_results.as_object().is_none() {
|
||||
app.data.radarr_data.indexer_test_errors = Some(
|
||||
test_results.as_array().unwrap()[0]
|
||||
.get("errorMessage")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
} else {
|
||||
app.data.radarr_data.indexer_test_errors = Some(String::new());
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn test_all_radarr_indexers(
|
||||
&mut self,
|
||||
) -> Result<Vec<IndexerTestResult>> {
|
||||
info!("Testing all Radarr indexers");
|
||||
let event = RadarrEvent::TestAllIndexers;
|
||||
|
||||
let mut request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, None, None, None)
|
||||
.await;
|
||||
request_props.ignore_status_code = true;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<IndexerTestResult>>(request_props, |test_results, mut app| {
|
||||
let mut test_all_indexer_results = StatefulTable::default();
|
||||
let indexers = app.data.radarr_data.indexers.items.clone();
|
||||
let modal_test_results = test_results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
let name = indexers
|
||||
.iter()
|
||||
.filter(|&indexer| indexer.id == result.id)
|
||||
.map(|indexer| indexer.name.clone())
|
||||
.nth(0)
|
||||
.unwrap_or_default();
|
||||
let validation_failures = result
|
||||
.validation_failures
|
||||
.iter()
|
||||
.map(|failure| {
|
||||
format!(
|
||||
"Failure for field '{}': {}",
|
||||
failure.property_name, failure.error_message
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
IndexerTestResultModalItem {
|
||||
name: name.unwrap_or_default(),
|
||||
is_valid: result.is_valid,
|
||||
validation_failures: validation_failures.into(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
test_all_indexer_results.set_items(modal_test_results);
|
||||
app.data.radarr_data.indexer_test_all_results = Some(test_all_indexer_results);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,980 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::{IndexerSettings, RadarrSerdeable};
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::radarr_network_test_utils::test_utils::{
|
||||
indexer, indexer_settings,
|
||||
};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, NetworkResource, RequestMethod};
|
||||
use bimap::BiMap;
|
||||
use mockito::Matcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_radarr_indexer_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::DeleteIndexer(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::DeleteIndexer(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_all_radarr_indexer_settings_event() {
|
||||
let indexer_settings_json = json!({
|
||||
"minimumAge": 0,
|
||||
"maximumSize": 0,
|
||||
"retention": 0,
|
||||
"rssSyncInterval": 60,
|
||||
"preferIndexerFlags": false,
|
||||
"availabilityDelay": 0,
|
||||
"allowHardcodedSubs": true,
|
||||
"whitelistedHardcodedSubs": "",
|
||||
"id": 1
|
||||
});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Put,
|
||||
Some(indexer_settings_json),
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::EditAllIndexerSettings(indexer_settings()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditAllIndexerSettings(indexer_settings()))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_radarr_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": 0,
|
||||
"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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditIndexer(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_radarr_indexer_event_does_not_overwrite_tags_vec_if_tag_input_string_is_none(
|
||||
) {
|
||||
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": 0,
|
||||
"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(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditIndexer(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_radarr_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": 0,
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://localhost:9696/1/",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "test1234",
|
||||
},
|
||||
],
|
||||
"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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditIndexer(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_radarr_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": 0,
|
||||
"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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::EditIndexer(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_radarr_indexer_event_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,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).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_radarr_event(RadarrEvent::EditIndexer(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_radarr_indexer_event_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,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_edit_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1?forceSave=true",
|
||||
RadarrEvent::EditIndexer(edit_indexer_params.clone()).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_radarr_event(RadarrEvent::EditIndexer(edit_indexer_params))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_details_server.assert_async().await;
|
||||
async_edit_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_indexers_event() {
|
||||
let indexers_response_json = json!([{
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"supportsRss": true,
|
||||
"supportsSearch": true,
|
||||
"protocol": "torrent",
|
||||
"priority": 25,
|
||||
"downloadClientId": 0,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"implementationName": "Torznab",
|
||||
"implementation": "Torznab",
|
||||
"configContract": "TorznabSettings",
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
}]);
|
||||
let response: Vec<Indexer> = serde_json::from_value(indexers_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexers_response_json),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Indexers(indexers) = network
|
||||
.handle_radarr_event(RadarrEvent::GetIndexers)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.indexers.items,
|
||||
vec![indexer()]
|
||||
);
|
||||
assert_eq!(indexers, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_all_indexer_settings_event() {
|
||||
let indexer_settings_response_json = json!({
|
||||
"minimumAge": 0,
|
||||
"maximumSize": 0,
|
||||
"retention": 0,
|
||||
"rssSyncInterval": 60,
|
||||
"preferIndexerFlags": false,
|
||||
"availabilityDelay": 0,
|
||||
"allowHardcodedSubs": true,
|
||||
"whitelistedHardcodedSubs": "",
|
||||
"id": 1
|
||||
});
|
||||
let response: IndexerSettings =
|
||||
serde_json::from_value(indexer_settings_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_settings_response_json),
|
||||
None,
|
||||
RadarrEvent::GetAllIndexerSettings,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::IndexerSettings(settings) = network
|
||||
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.indexer_settings,
|
||||
Some(indexer_settings())
|
||||
);
|
||||
assert_eq!(settings, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_all_indexer_settings_event_no_op_if_already_present() {
|
||||
let indexer_settings_response_json = json!({
|
||||
"minimumAge": 0,
|
||||
"maximumSize": 0,
|
||||
"retention": 0,
|
||||
"rssSyncInterval": 60,
|
||||
"preferIndexerFlags": false,
|
||||
"availabilityDelay": 0,
|
||||
"allowHardcodedSubs": true,
|
||||
"whitelistedHardcodedSubs": "",
|
||||
"id": 1
|
||||
});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_settings_response_json),
|
||||
None,
|
||||
RadarrEvent::GetAllIndexerSettings,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.indexer_settings = Some(IndexerSettings::default());
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::GetAllIndexerSettings)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.indexer_settings,
|
||||
Some(IndexerSettings::default())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_radarr_indexer_event_error() {
|
||||
let indexer_details_json = json!({
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
});
|
||||
let response_json = json!([
|
||||
{
|
||||
"isWarning": false,
|
||||
"propertyName": "",
|
||||
"errorMessage": "test failure",
|
||||
"severity": "error"
|
||||
}]);
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json.clone()),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_test_server = server
|
||||
.mock(
|
||||
"POST",
|
||||
format!("/api/v3{}", RadarrEvent::TestIndexer(1).resource()).as_str(),
|
||||
)
|
||||
.with_status(400)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(indexer_details_json.clone()))
|
||||
.with_body(response_json.to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Value(value) = network
|
||||
.handle_radarr_event(RadarrEvent::TestIndexer(1))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_details_server.assert_async().await;
|
||||
async_test_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.indexer_test_errors,
|
||||
Some("\"test failure\"".to_owned())
|
||||
);
|
||||
assert_eq!(value, response_json)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_radarr_indexer_event_success() {
|
||||
let indexer_details_json = json!({
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
});
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexer_details_json.clone()),
|
||||
None,
|
||||
RadarrEvent::GetIndexers,
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_test_server = server
|
||||
.mock(
|
||||
"POST",
|
||||
format!("/api/v3{}", RadarrEvent::TestIndexer(1).resource()).as_str(),
|
||||
)
|
||||
.with_status(200)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(indexer_details_json.clone()))
|
||||
.with_body("{}")
|
||||
.create_async()
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Value(value) = network
|
||||
.handle_radarr_event(RadarrEvent::TestIndexer(1))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_details_server.assert_async().await;
|
||||
async_test_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.indexer_test_errors,
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(value, json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_all_radarr_indexers_event() {
|
||||
let indexers = vec![
|
||||
Indexer {
|
||||
id: 1,
|
||||
name: Some("Test 1".to_owned()),
|
||||
..Indexer::default()
|
||||
},
|
||||
Indexer {
|
||||
id: 2,
|
||||
name: Some("Test 2".to_owned()),
|
||||
..Indexer::default()
|
||||
},
|
||||
];
|
||||
let indexer_test_results_modal_items = vec![
|
||||
IndexerTestResultModalItem {
|
||||
name: "Test 1".to_owned(),
|
||||
is_valid: true,
|
||||
validation_failures: HorizontallyScrollableText::default(),
|
||||
},
|
||||
IndexerTestResultModalItem {
|
||||
name: "Test 2".to_owned(),
|
||||
is_valid: false,
|
||||
validation_failures: "Failure for field 'test field 1': test error message, Failure for field 'test field 2': test error message 2".into(),
|
||||
},
|
||||
];
|
||||
let response_json = json!([
|
||||
{
|
||||
"id": 1,
|
||||
"isValid": true,
|
||||
"validationFailures": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"isValid": false,
|
||||
"validationFailures": [
|
||||
{
|
||||
"propertyName": "test field 1",
|
||||
"errorMessage": "test error message",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"propertyName": "test field 2",
|
||||
"errorMessage": "test error message 2",
|
||||
"severity": "error"
|
||||
},
|
||||
]
|
||||
}]);
|
||||
let response: Vec<IndexerTestResult> = serde_json::from_value(response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
None,
|
||||
Some(response_json),
|
||||
Some(400),
|
||||
RadarrEvent::TestAllIndexers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.indexers
|
||||
.set_items(indexers);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::IndexerTestResults(results) = network
|
||||
.handle_radarr_event(RadarrEvent::TestAllIndexers)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.indexer_test_all_results
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.indexer_test_all_results
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.items,
|
||||
indexer_test_results_modal_items
|
||||
);
|
||||
assert_eq!(results, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, AddMovieSearchResult, Credit, CreditType, DeleteMovieParams, DownloadRecord,
|
||||
EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease,
|
||||
RadarrReleaseDownloadBody,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::models::{Route, ScrollableText};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use crate::utils::{convert_runtime, convert_to_gb};
|
||||
use anyhow::Result;
|
||||
use indoc::formatdoc;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{json, Value};
|
||||
use urlencoding::encode;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_library_network_tests.rs"]
|
||||
mod radarr_library_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn add_movie(
|
||||
&mut self,
|
||||
mut add_movie_body: AddMovieBody,
|
||||
) -> Result<Value> {
|
||||
info!("Adding new movie to Radarr");
|
||||
let event = RadarrEvent::AddMovie(AddMovieBody::default());
|
||||
if let Some(tag_input_str) = add_movie_body.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tag_input_str).await;
|
||||
add_movie_body.tags = tag_ids_vec;
|
||||
}
|
||||
|
||||
debug!("Add movie body: {add_movie_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(add_movie_body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<AddMovieBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn delete_movie(
|
||||
&mut self,
|
||||
delete_movie_params: DeleteMovieParams,
|
||||
) -> Result<()> {
|
||||
let event = RadarrEvent::DeleteMovie(DeleteMovieParams::default());
|
||||
let DeleteMovieParams {
|
||||
id,
|
||||
delete_movie_files,
|
||||
add_list_exclusion,
|
||||
} = delete_movie_params;
|
||||
info!("Deleting Radarr movie with ID: {id} with deleteFiles={delete_movie_files} and addImportExclusion={add_list_exclusion}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
Some(format!(
|
||||
"deleteFiles={delete_movie_files}&addImportExclusion={add_list_exclusion}"
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn download_radarr_release(
|
||||
&mut self,
|
||||
params: RadarrReleaseDownloadBody,
|
||||
) -> Result<Value> {
|
||||
let event = RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default());
|
||||
info!("Downloading Radarr release with params: {params:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(params), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<RadarrReleaseDownloadBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn edit_movie(
|
||||
&mut self,
|
||||
mut edit_movie_params: EditMovieParams,
|
||||
) -> Result<()> {
|
||||
info!("Editing Radarr movie");
|
||||
let movie_id = edit_movie_params.movie_id;
|
||||
let detail_event = RadarrEvent::GetMovieDetails(movie_id);
|
||||
let event = RadarrEvent::EditMovie(EditMovieParams::default());
|
||||
if let Some(tag_input_str) = edit_movie_params.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tag_input_str).await;
|
||||
edit_movie_params.tags = Some(tag_ids_vec);
|
||||
}
|
||||
|
||||
info!("Fetching movie details for movie with ID: {movie_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{movie_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_movie_body, _| {
|
||||
response = detailed_movie_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing edit movie body");
|
||||
|
||||
let mut detailed_movie_body: Value = serde_json::from_str(&response)?;
|
||||
let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = {
|
||||
let monitored = edit_movie_params.monitored.unwrap_or(
|
||||
detailed_movie_body["monitored"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'monitored'"),
|
||||
);
|
||||
let minimum_availability = edit_movie_params
|
||||
.minimum_availability
|
||||
.unwrap_or_else(|| {
|
||||
serde_json::from_value(detailed_movie_body["minimumAvailability"].clone())
|
||||
.expect("Unable to deserialize 'minimumAvailability'")
|
||||
})
|
||||
.to_string();
|
||||
let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| {
|
||||
detailed_movie_body["qualityProfileId"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'qualityProfileId'")
|
||||
});
|
||||
let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| {
|
||||
detailed_movie_body["path"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'path'")
|
||||
.to_owned()
|
||||
});
|
||||
let tags = if edit_movie_params.clear_tags {
|
||||
vec![]
|
||||
} else {
|
||||
edit_movie_params.tags.unwrap_or(
|
||||
detailed_movie_body["tags"]
|
||||
.as_array()
|
||||
.expect("Unable to deserialize 'tags'")
|
||||
.iter()
|
||||
.map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
monitored,
|
||||
minimum_availability,
|
||||
quality_profile_id,
|
||||
root_folder_path,
|
||||
tags,
|
||||
)
|
||||
};
|
||||
|
||||
*detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored);
|
||||
*detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability);
|
||||
*detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
|
||||
*detailed_movie_body.get_mut("path").unwrap() = json!(root_folder_path);
|
||||
*detailed_movie_body.get_mut("tags").unwrap() = json!(tags);
|
||||
|
||||
debug!("Edit movie body: {detailed_movie_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_movie_body),
|
||||
Some(format!("/{movie_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_credits(
|
||||
&mut self,
|
||||
movie_id: i64,
|
||||
) -> Result<Vec<Credit>> {
|
||||
info!("Fetching Radarr movie credits");
|
||||
let event = RadarrEvent::GetMovieCredits(movie_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("movieId={movie_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| {
|
||||
let cast_vec: Vec<Credit> = credit_vec
|
||||
.iter()
|
||||
.filter(|&credit| credit.credit_type == CreditType::Cast)
|
||||
.cloned()
|
||||
.collect();
|
||||
let crew_vec: Vec<Credit> = credit_vec
|
||||
.iter()
|
||||
.filter(|&credit| credit.credit_type == CreditType::Crew)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if app.data.radarr_data.movie_details_modal.is_none() {
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
|
||||
}
|
||||
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_cast
|
||||
.set_items(cast_vec);
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_crew
|
||||
.set_items(crew_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_movies(&mut self) -> Result<Vec<Movie>> {
|
||||
info!("Fetching Radarr library");
|
||||
let event = RadarrEvent::GetMovies;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Movie>>(request_props, |mut movie_vec, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _)
|
||||
) {
|
||||
movie_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.radarr_data.movies.set_items(movie_vec);
|
||||
app.data.radarr_data.movies.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_movie_details(
|
||||
&mut self,
|
||||
movie_id: i64,
|
||||
) -> Result<Movie> {
|
||||
info!("Fetching Radarr movie details");
|
||||
let event = RadarrEvent::GetMovieDetails(movie_id);
|
||||
|
||||
info!("Fetching movie details for movie with ID: {movie_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{movie_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Movie>(request_props, |movie_response, mut app| {
|
||||
let Movie {
|
||||
id,
|
||||
title,
|
||||
year,
|
||||
overview,
|
||||
path,
|
||||
studio,
|
||||
has_file,
|
||||
quality_profile_id,
|
||||
size_on_disk,
|
||||
genres,
|
||||
runtime,
|
||||
certification,
|
||||
ratings,
|
||||
movie_file,
|
||||
collection,
|
||||
..
|
||||
} = movie_response;
|
||||
let (hours, minutes) = convert_runtime(runtime);
|
||||
let size = convert_to_gb(size_on_disk);
|
||||
let studio = studio.clone().unwrap_or_default();
|
||||
let quality_profile = app
|
||||
.data
|
||||
.radarr_data
|
||||
.quality_profile_map
|
||||
.get_by_left(&quality_profile_id)
|
||||
.unwrap_or(&"".to_owned())
|
||||
.to_owned();
|
||||
let imdb_rating = if let Some(rating) = ratings.imdb {
|
||||
if let Some(value) = rating.value.as_f64() {
|
||||
format!("{value:.1}")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let tmdb_rating = if let Some(rating) = ratings.tmdb {
|
||||
if let Some(value) = rating.value.as_f64() {
|
||||
format!("{}%", (value * 10f64).ceil())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let rotten_tomatoes_rating = if let Some(rating) = ratings.rotten_tomatoes {
|
||||
if let Some(value) = rating.value.as_u64() {
|
||||
format!("{value}%")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id);
|
||||
let collection = collection.unwrap_or_default();
|
||||
|
||||
let mut movie_details_modal = MovieDetailsModal {
|
||||
movie_details: ScrollableText::with_string(formatdoc!(
|
||||
"Title: {title}
|
||||
Year: {year}
|
||||
Runtime: {hours}h {minutes}m
|
||||
Rating: {}
|
||||
Collection: {}
|
||||
Status: {status}
|
||||
Description: {overview}
|
||||
TMDB: {tmdb_rating}
|
||||
IMDB: {imdb_rating}
|
||||
Rotten Tomatoes: {rotten_tomatoes_rating}
|
||||
Quality Profile: {quality_profile}
|
||||
Size: {size:.2} GB
|
||||
Path: {path}
|
||||
Studio: {studio}
|
||||
Genres: {}",
|
||||
certification.unwrap_or_default(),
|
||||
collection
|
||||
.title
|
||||
.as_ref()
|
||||
.unwrap_or(&String::new())
|
||||
.to_owned(),
|
||||
genres.join(", ")
|
||||
)),
|
||||
..MovieDetailsModal::default()
|
||||
};
|
||||
|
||||
if let Some(file) = movie_file {
|
||||
movie_details_modal.file_details = formatdoc!(
|
||||
"Relative Path: {}
|
||||
Absolute Path: {}
|
||||
Size: {size:.2} GB
|
||||
Date Added: {}",
|
||||
file.relative_path,
|
||||
file.path,
|
||||
file.date_added
|
||||
);
|
||||
|
||||
if let Some(media_info) = file.media_info {
|
||||
movie_details_modal.audio_details = formatdoc!(
|
||||
"Bitrate: {}
|
||||
Channels: {:.1}
|
||||
Codec: {}
|
||||
Languages: {}
|
||||
Stream Count: {}",
|
||||
media_info.audio_bitrate,
|
||||
media_info.audio_channels.as_f64().unwrap(),
|
||||
media_info.audio_codec.unwrap_or_default(),
|
||||
media_info.audio_languages.unwrap_or_default(),
|
||||
media_info.audio_stream_count
|
||||
);
|
||||
|
||||
movie_details_modal.video_details = formatdoc!(
|
||||
"Bit Depth: {}
|
||||
Bitrate: {}
|
||||
Codec: {}
|
||||
FPS: {}
|
||||
Resolution: {}
|
||||
Scan Type: {}
|
||||
Runtime: {}",
|
||||
media_info.video_bit_depth,
|
||||
media_info.video_bitrate,
|
||||
media_info.video_codec.unwrap_or_default(),
|
||||
media_info.video_fps.as_f64().unwrap(),
|
||||
media_info.resolution,
|
||||
media_info.scan_type,
|
||||
media_info.run_time
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_movie_history(
|
||||
&mut self,
|
||||
movie_id: i64,
|
||||
) -> Result<Vec<MovieHistoryItem>> {
|
||||
info!("Fetching Radarr movie history");
|
||||
let event = RadarrEvent::GetMovieHistory(movie_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("movieId={movie_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<MovieHistoryItem>>(request_props, |movie_history_vec, mut app| {
|
||||
let mut reversed_movie_history_vec = movie_history_vec.to_vec();
|
||||
reversed_movie_history_vec.reverse();
|
||||
|
||||
if app.data.radarr_data.movie_details_modal.is_none() {
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default())
|
||||
}
|
||||
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_history
|
||||
.set_items(reversed_movie_history_vec)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_movie_releases(
|
||||
&mut self,
|
||||
movie_id: i64,
|
||||
) -> Result<Vec<RadarrRelease>> {
|
||||
info!("Fetching releases for movie with ID: {movie_id}");
|
||||
let event = RadarrEvent::GetReleases(movie_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("movieId={movie_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RadarrRelease>>(request_props, |release_vec, mut app| {
|
||||
if app.data.radarr_data.movie_details_modal.is_none() {
|
||||
app.data.radarr_data.movie_details_modal = Some(MovieDetailsModal::default());
|
||||
}
|
||||
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.movie_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.movie_releases
|
||||
.set_items(release_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn search_movie(
|
||||
&mut self,
|
||||
query: String,
|
||||
) -> Result<Vec<AddMovieSearchResult>> {
|
||||
info!("Searching for specific Radarr movie");
|
||||
let event = RadarrEvent::SearchNewMovie(String::new());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("term={}", encode(&query))),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<AddMovieSearchResult>>(request_props, |movie_vec, mut app| {
|
||||
if movie_vec.is_empty() {
|
||||
app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into());
|
||||
} else if let Some(add_searched_movies) = app.data.radarr_data.add_searched_movies.as_mut()
|
||||
{
|
||||
add_searched_movies.set_items(movie_vec);
|
||||
} else {
|
||||
let mut add_searched_movies = StatefulTable::default();
|
||||
add_searched_movies.set_items(movie_vec);
|
||||
app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network) async fn toggle_movie_monitoring(&mut self, movie_id: i64) -> Result<()> {
|
||||
let event = RadarrEvent::ToggleMovieMonitoring(movie_id);
|
||||
|
||||
let detail_event = RadarrEvent::GetMovieDetails(movie_id);
|
||||
info!("Toggling movie monitoring for movie with ID: {movie_id}");
|
||||
info!("Fetching movie details for movie with ID: {movie_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{movie_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut response = String::new();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_movie_body, _| {
|
||||
response = detailed_movie_body.to_string()
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Constructing toggle movie monitoring body");
|
||||
|
||||
match serde_json::from_str::<Value>(&response) {
|
||||
Ok(mut detailed_movie_body) => {
|
||||
let monitored = detailed_movie_body
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
|
||||
*detailed_movie_body.get_mut("monitored").unwrap() = json!(!monitored);
|
||||
|
||||
debug!("Toggle movie monitoring body: {detailed_movie_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Put,
|
||||
Some(detailed_movie_body),
|
||||
Some(format!("/{movie_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request for detailed movie body was interrupted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network) async fn trigger_automatic_movie_search(
|
||||
&mut self,
|
||||
movie_id: i64,
|
||||
) -> Result<Value> {
|
||||
let event = RadarrEvent::TriggerAutomaticSearch(movie_id);
|
||||
info!("Searching indexers for movie with ID: {movie_id}");
|
||||
let body = MovieCommandBody {
|
||||
name: "MoviesSearch".to_owned(),
|
||||
movie_ids: vec![movie_id],
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network) async fn update_all_movies(&mut self) -> Result<Value> {
|
||||
info!("Updating all movies");
|
||||
let event = RadarrEvent::UpdateAllMovies;
|
||||
let body = MovieCommandBody {
|
||||
name: "RefreshMovie".to_owned(),
|
||||
movie_ids: Vec::new(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network) async fn update_and_scan_movie(&mut self, movie_id: i64) -> Result<Value> {
|
||||
let event = RadarrEvent::UpdateAndScan(movie_id);
|
||||
info!("Updating and scanning movie with ID: {movie_id}");
|
||||
let body = MovieCommandBody {
|
||||
name: "RefreshMovie".to_owned(),
|
||||
movie_ids: vec![movie_id],
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network::library) fn get_movie_status(
|
||||
has_file: bool,
|
||||
downloads_vec: &[DownloadRecord],
|
||||
movie_id: i64,
|
||||
) -> String {
|
||||
if !has_file {
|
||||
if let Some(download) = downloads_vec
|
||||
.iter()
|
||||
.find(|&download| download.movie_id == movie_id)
|
||||
{
|
||||
if download.status == "downloading" {
|
||||
return "Downloading".to_owned();
|
||||
}
|
||||
|
||||
if download.status == "completed" {
|
||||
return "Awaiting Import".to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
return "Missing".to_owned();
|
||||
}
|
||||
|
||||
"Downloaded".to_owned()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
use anyhow::Result;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use log::info;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieBody, DeleteMovieParams, EditCollectionParams, EditMovieParams, IndexerSettings,
|
||||
RadarrReleaseDownloadBody, RadarrSerdeable, RadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_models::{AddRootFolderBody, EditIndexerParams, QualityProfile, Tag};
|
||||
use crate::network::{Network, NetworkEvent, RequestMethod};
|
||||
|
||||
use super::NetworkResource;
|
||||
|
||||
mod blocklist;
|
||||
mod collections;
|
||||
mod downloads;
|
||||
mod indexers;
|
||||
mod library;
|
||||
mod root_folders;
|
||||
mod system;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_network_tests.rs"]
|
||||
mod radarr_network_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_network_test_utils.rs"]
|
||||
mod radarr_network_test_utils;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum RadarrEvent {
|
||||
AddMovie(AddMovieBody),
|
||||
AddRootFolder(AddRootFolderBody),
|
||||
AddTag(String),
|
||||
ClearBlocklist,
|
||||
DeleteBlocklistItem(i64),
|
||||
DeleteDownload(i64),
|
||||
DeleteIndexer(i64),
|
||||
DeleteMovie(DeleteMovieParams),
|
||||
DeleteRootFolder(i64),
|
||||
DeleteTag(i64),
|
||||
DownloadRelease(RadarrReleaseDownloadBody),
|
||||
EditAllIndexerSettings(IndexerSettings),
|
||||
EditCollection(EditCollectionParams),
|
||||
EditIndexer(EditIndexerParams),
|
||||
EditMovie(EditMovieParams),
|
||||
GetBlocklist,
|
||||
GetCollections,
|
||||
GetDownloads(u64),
|
||||
GetHostConfig,
|
||||
GetIndexers,
|
||||
GetAllIndexerSettings,
|
||||
GetLogs(u64),
|
||||
GetMovieCredits(i64),
|
||||
GetMovieDetails(i64),
|
||||
GetMovieHistory(i64),
|
||||
GetMovies,
|
||||
GetDiskSpace,
|
||||
GetQualityProfiles,
|
||||
GetQueuedEvents,
|
||||
GetReleases(i64),
|
||||
GetRootFolders,
|
||||
GetSecurityConfig,
|
||||
GetStatus,
|
||||
GetTags,
|
||||
GetTasks,
|
||||
GetUpdates,
|
||||
HealthCheck,
|
||||
SearchNewMovie(String),
|
||||
StartTask(RadarrTaskName),
|
||||
TestIndexer(i64),
|
||||
TestAllIndexers,
|
||||
ToggleMovieMonitoring(i64),
|
||||
TriggerAutomaticSearch(i64),
|
||||
UpdateAllMovies,
|
||||
UpdateAndScan(i64),
|
||||
UpdateCollections,
|
||||
UpdateDownloads,
|
||||
}
|
||||
|
||||
impl NetworkResource for RadarrEvent {
|
||||
fn resource(&self) -> &'static str {
|
||||
match &self {
|
||||
RadarrEvent::ClearBlocklist => "/blocklist/bulk",
|
||||
RadarrEvent::DeleteBlocklistItem(_) => "/blocklist",
|
||||
RadarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
|
||||
RadarrEvent::GetCollections | RadarrEvent::EditCollection(_) => "/collection",
|
||||
RadarrEvent::GetDownloads(_) | RadarrEvent::DeleteDownload(_) => "/queue",
|
||||
RadarrEvent::GetHostConfig | RadarrEvent::GetSecurityConfig => "/config/host",
|
||||
RadarrEvent::GetIndexers | RadarrEvent::EditIndexer(_) | RadarrEvent::DeleteIndexer(_) => {
|
||||
"/indexer"
|
||||
}
|
||||
RadarrEvent::GetAllIndexerSettings | RadarrEvent::EditAllIndexerSettings(_) => {
|
||||
"/config/indexer"
|
||||
}
|
||||
RadarrEvent::GetLogs(_) => "/log",
|
||||
RadarrEvent::AddMovie(_)
|
||||
| RadarrEvent::EditMovie(_)
|
||||
| RadarrEvent::GetMovies
|
||||
| RadarrEvent::GetMovieDetails(_)
|
||||
| RadarrEvent::DeleteMovie(_)
|
||||
| RadarrEvent::ToggleMovieMonitoring(_) => "/movie",
|
||||
RadarrEvent::SearchNewMovie(_) => "/movie/lookup",
|
||||
RadarrEvent::GetMovieCredits(_) => "/credit",
|
||||
RadarrEvent::GetMovieHistory(_) => "/history/movie",
|
||||
RadarrEvent::GetDiskSpace => "/diskspace",
|
||||
RadarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||
RadarrEvent::GetReleases(_) | RadarrEvent::DownloadRelease(_) => "/release",
|
||||
RadarrEvent::AddRootFolder(_)
|
||||
| RadarrEvent::GetRootFolders
|
||||
| RadarrEvent::DeleteRootFolder(_) => "/rootfolder",
|
||||
RadarrEvent::GetStatus => "/system/status",
|
||||
RadarrEvent::GetTags | RadarrEvent::AddTag(_) | RadarrEvent::DeleteTag(_) => "/tag",
|
||||
RadarrEvent::GetTasks => "/system/task",
|
||||
RadarrEvent::GetUpdates => "/update",
|
||||
RadarrEvent::TestIndexer(_) => "/indexer/test",
|
||||
RadarrEvent::TestAllIndexers => "/indexer/testall",
|
||||
RadarrEvent::StartTask(_)
|
||||
| RadarrEvent::GetQueuedEvents
|
||||
| RadarrEvent::TriggerAutomaticSearch(_)
|
||||
| RadarrEvent::UpdateAndScan(_)
|
||||
| RadarrEvent::UpdateAllMovies
|
||||
| RadarrEvent::UpdateDownloads
|
||||
| RadarrEvent::UpdateCollections => "/command",
|
||||
RadarrEvent::HealthCheck => "/health",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RadarrEvent> for NetworkEvent {
|
||||
fn from(radarr_event: RadarrEvent) -> Self {
|
||||
NetworkEvent::Radarr(radarr_event)
|
||||
}
|
||||
}
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub async fn handle_radarr_event(
|
||||
&mut self,
|
||||
radarr_event: RadarrEvent,
|
||||
) -> Result<RadarrSerdeable> {
|
||||
match radarr_event {
|
||||
RadarrEvent::AddMovie(body) => self.add_movie(body).await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::AddRootFolder(path) => self
|
||||
.add_radarr_root_folder(path)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::AddTag(tag) => self.add_radarr_tag(tag).await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::ClearBlocklist => self
|
||||
.clear_radarr_blocklist()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DeleteBlocklistItem(blocklist_item_id) => self
|
||||
.delete_radarr_blocklist_item(blocklist_item_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DeleteDownload(download_id) => self
|
||||
.delete_radarr_download(download_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DeleteIndexer(indexer_id) => self
|
||||
.delete_radarr_indexer(indexer_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DeleteMovie(params) => {
|
||||
self.delete_movie(params).await.map(RadarrSerdeable::from)
|
||||
}
|
||||
RadarrEvent::DeleteRootFolder(root_folder_id) => self
|
||||
.delete_radarr_root_folder(root_folder_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DeleteTag(tag_id) => self
|
||||
.delete_radarr_tag(tag_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::DownloadRelease(params) => self
|
||||
.download_radarr_release(params)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::EditAllIndexerSettings(params) => self
|
||||
.edit_all_radarr_indexer_settings(params)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::EditCollection(params) => self
|
||||
.edit_collection(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()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetBlocklist => self.get_radarr_blocklist().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetCollections => self.get_collections().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetDownloads(count) => self
|
||||
.get_radarr_downloads(count)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetHostConfig => self
|
||||
.get_radarr_host_config()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetIndexers => self.get_radarr_indexers().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetLogs(events) => self
|
||||
.get_radarr_logs(events)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetMovieCredits(movie_id) => {
|
||||
self.get_credits(movie_id).await.map(RadarrSerdeable::from)
|
||||
}
|
||||
RadarrEvent::GetMovieDetails(movie_id) => self
|
||||
.get_movie_details(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetMovieHistory(movie_id) => self
|
||||
.get_movie_history(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetMovies => self.get_movies().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetDiskSpace => self.get_radarr_diskspace().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetQualityProfiles => self
|
||||
.get_radarr_quality_profiles()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetQueuedEvents => self
|
||||
.get_queued_radarr_events()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetReleases(movie_id) => self
|
||||
.get_movie_releases(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetRootFolders => self
|
||||
.get_radarr_root_folders()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetSecurityConfig => self
|
||||
.get_radarr_security_config()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetStatus => self.get_radarr_status().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetTags => self.get_radarr_tags().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetTasks => self.get_radarr_tasks().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::GetUpdates => self.get_radarr_updates().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::HealthCheck => self
|
||||
.get_radarr_healthcheck()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::SearchNewMovie(query) => {
|
||||
self.search_movie(query).await.map(RadarrSerdeable::from)
|
||||
}
|
||||
RadarrEvent::StartTask(task_name) => self
|
||||
.start_radarr_task(task_name)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::TestIndexer(indexer_id) => self
|
||||
.test_radarr_indexer(indexer_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::TestAllIndexers => self
|
||||
.test_all_radarr_indexers()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::ToggleMovieMonitoring(movie_id) => self
|
||||
.toggle_movie_monitoring(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::TriggerAutomaticSearch(movie_id) => self
|
||||
.trigger_automatic_movie_search(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::UpdateAllMovies => self.update_all_movies().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::UpdateAndScan(movie_id) => self
|
||||
.update_and_scan_movie(movie_id)
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
RadarrEvent::UpdateCollections => self.update_collections().await.map(RadarrSerdeable::from),
|
||||
RadarrEvent::UpdateDownloads => self
|
||||
.update_radarr_downloads()
|
||||
.await
|
||||
.map(RadarrSerdeable::from),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn add_radarr_tag(
|
||||
&mut self,
|
||||
tag: String,
|
||||
) -> Result<Tag> {
|
||||
info!("Adding a new Radarr tag");
|
||||
let event = RadarrEvent::AddTag(String::new());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": tag })),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, Tag>(request_props, |tag, mut app| {
|
||||
app.data.radarr_data.tags_map.insert(tag.id, tag.label);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn delete_radarr_tag(
|
||||
&mut self,
|
||||
id: i64,
|
||||
) -> Result<()> {
|
||||
info!("Deleting Radarr tag with id: {id}");
|
||||
let event = RadarrEvent::DeleteTag(id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_radarr_healthcheck(&mut self) -> Result<()> {
|
||||
info!("Performing Radarr health check");
|
||||
let event = RadarrEvent::HealthCheck;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_radarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
|
||||
info!("Fetching Radarr quality profiles");
|
||||
let event = RadarrEvent::GetQualityProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||
app.data.radarr_data.quality_profile_map = quality_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_radarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||
info!("Fetching Radarr tags");
|
||||
let event = RadarrEvent::GetTags;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||
app.data.radarr_data.tags_map = tags_vec
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.label))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn extract_and_add_radarr_tag_ids_vec(
|
||||
&mut self,
|
||||
edit_tags: &str,
|
||||
) -> Vec<i64> {
|
||||
let missing_tags_vec = {
|
||||
let tags_map = &self.app.lock().await.data.radarr_data.tags_map;
|
||||
edit_tags
|
||||
.split(',')
|
||||
.filter(|&tag| {
|
||||
!tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
};
|
||||
|
||||
for tag in missing_tags_vec {
|
||||
self
|
||||
.add_radarr_tag(tag.trim().to_owned())
|
||||
.await
|
||||
.expect("Unable to add tag");
|
||||
}
|
||||
|
||||
let app = self.app.lock().await;
|
||||
edit_tags
|
||||
.split(',')
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.map(|tag| {
|
||||
*app
|
||||
.data
|
||||
.radarr_data
|
||||
.tags_map
|
||||
.get_by_right(tag.to_lowercase().trim())
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
#[cfg(test)]
|
||||
pub(in crate::network::radarr_network) mod test_utils {
|
||||
use crate::models::radarr_models::{
|
||||
AddMovieSearchResult, BlocklistItem, BlocklistItemMovie, Collection, CollectionMovie, Credit,
|
||||
CreditType, DownloadRecord, DownloadsResponse, IndexerSettings, MediaInfo, MinimumAvailability,
|
||||
Movie, MovieCollection, MovieFile, MovieHistoryItem, RadarrRelease, Rating, RatingsList,
|
||||
};
|
||||
use crate::models::servarr_models::{
|
||||
Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder,
|
||||
};
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use chrono::DateTime;
|
||||
use serde_json::{json, Number};
|
||||
|
||||
pub const MOVIE_JSON: &str = r#"{
|
||||
"id": 1,
|
||||
"title": "Test",
|
||||
"tmdbId": 1234,
|
||||
"originalLanguage": {
|
||||
"id": 1,
|
||||
"name": "English"
|
||||
},
|
||||
"sizeOnDisk": 3543348019,
|
||||
"status": "Downloaded",
|
||||
"overview": "Blah blah blah",
|
||||
"path": "/nfs/movies",
|
||||
"studio": "21st Century Alex",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"year": 2023,
|
||||
"monitored": true,
|
||||
"hasFile": true,
|
||||
"runtime": 120,
|
||||
"qualityProfileId": 2222,
|
||||
"minimumAvailability": "announced",
|
||||
"certification": "R",
|
||||
"tags": [1],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
},
|
||||
"movieFile": {
|
||||
"relativePath": "Test.mkv",
|
||||
"path": "/nfs/movies/Test.mkv",
|
||||
"dateAdded": "2022-12-30T07:37:56Z",
|
||||
"mediaInfo": {
|
||||
"audioBitrate": 0,
|
||||
"audioChannels": 7.1,
|
||||
"audioCodec": "AAC",
|
||||
"audioLanguages": "eng",
|
||||
"audioStreamCount": 1,
|
||||
"videoBitDepth": 10,
|
||||
"videoBitrate": 0,
|
||||
"videoCodec": "x265",
|
||||
"videoFps": 23.976,
|
||||
"resolution": "1920x804",
|
||||
"runTime": "2:00:00",
|
||||
"scanType": "Progressive"
|
||||
}
|
||||
},
|
||||
"collection": {
|
||||
"id": 123,
|
||||
"title": "Test Collection",
|
||||
"rootFolderPath": "/nfs/movies",
|
||||
"searchOnAdd": true,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "released",
|
||||
"overview": "Collection blah blah blah",
|
||||
"qualityProfileId": 2222,
|
||||
"movies": [
|
||||
{
|
||||
"title": "Test",
|
||||
"overview": "Collection blah blah blah",
|
||||
"year": 2023,
|
||||
"runtime": 120,
|
||||
"tmdbId": 1234,
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"ratings": {
|
||||
"imdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"tmdb": {
|
||||
"value": 9.9
|
||||
},
|
||||
"rottenTomatoes": {
|
||||
"value": 9.9
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub fn language() -> Language {
|
||||
Language {
|
||||
id: 1,
|
||||
name: "English".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn genres() -> Vec<String> {
|
||||
vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()]
|
||||
}
|
||||
|
||||
pub fn rating() -> Rating {
|
||||
Rating {
|
||||
value: Number::from_f64(9.9).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ratings_list() -> RatingsList {
|
||||
RatingsList {
|
||||
imdb: Some(rating()),
|
||||
tmdb: Some(rating()),
|
||||
rotten_tomatoes: Some(rating()),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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: Some("x265".to_owned()),
|
||||
video_fps: Number::from_f64(23.976).unwrap(),
|
||||
resolution: "1920x804".to_owned(),
|
||||
run_time: "2:00:00".to_owned(),
|
||||
scan_type: "Progressive".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movie_file() -> MovieFile {
|
||||
MovieFile {
|
||||
relative_path: "Test.mkv".to_owned(),
|
||||
path: "/nfs/movies/Test.mkv".to_owned(),
|
||||
date_added: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()),
|
||||
media_info: Some(media_info()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collection_movie() -> CollectionMovie {
|
||||
CollectionMovie {
|
||||
title: "Test".to_owned().into(),
|
||||
overview: "Collection blah blah blah".to_owned(),
|
||||
year: 2023,
|
||||
runtime: 120,
|
||||
tmdb_id: 1234,
|
||||
genres: genres(),
|
||||
ratings: ratings_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blocklist_item() -> BlocklistItem {
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
movie_id: 1,
|
||||
source_title: "z movie".to_owned(),
|
||||
languages: vec![language()],
|
||||
quality: quality_wrapper(),
|
||||
custom_formats: Some(vec![language()]),
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()),
|
||||
protocol: "usenet".to_owned(),
|
||||
indexer: "DrunkenSlug (Prowlarr)".to_owned(),
|
||||
message: "test message".to_owned(),
|
||||
movie: blocklist_item_movie(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blocklist_item_movie() -> BlocklistItemMovie {
|
||||
BlocklistItemMovie {
|
||||
title: "Test".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collection() -> Collection {
|
||||
Collection {
|
||||
id: 123,
|
||||
title: "Test Collection".to_owned().into(),
|
||||
root_folder_path: Some("/nfs/movies".to_owned()),
|
||||
search_on_add: true,
|
||||
monitored: true,
|
||||
minimum_availability: MinimumAvailability::Released,
|
||||
overview: Some("Collection blah blah blah".to_owned()),
|
||||
quality_profile_id: 2222,
|
||||
movies: Some(vec![collection_movie()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movie() -> Movie {
|
||||
Movie {
|
||||
id: 1,
|
||||
title: "Test".to_owned().into(),
|
||||
original_language: language(),
|
||||
size_on_disk: 3543348019,
|
||||
status: "Downloaded".to_owned(),
|
||||
overview: "Blah blah blah".to_owned(),
|
||||
path: "/nfs/movies".to_owned(),
|
||||
studio: Some("21st Century Alex".to_owned()),
|
||||
genres: genres(),
|
||||
year: 2023,
|
||||
monitored: true,
|
||||
has_file: true,
|
||||
runtime: 120,
|
||||
tmdb_id: 1234,
|
||||
quality_profile_id: 2222,
|
||||
minimum_availability: MinimumAvailability::Announced,
|
||||
certification: Some("R".to_owned()),
|
||||
tags: vec![Number::from(1)],
|
||||
ratings: ratings_list(),
|
||||
movie_file: Some(movie_file()),
|
||||
collection: Some(movie_collection()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movie_collection() -> MovieCollection {
|
||||
MovieCollection {
|
||||
title: Some("Test Collection".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rejections() -> Vec<String> {
|
||||
vec![
|
||||
"Unknown quality profile".to_owned(),
|
||||
"Release is already mapped".to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn quality() -> Quality {
|
||||
Quality {
|
||||
name: "HD - 1080p".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quality_wrapper() -> QualityWrapper {
|
||||
QualityWrapper { quality: quality() }
|
||||
}
|
||||
|
||||
pub fn release() -> RadarrRelease {
|
||||
RadarrRelease {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_movie_search_result() -> AddMovieSearchResult {
|
||||
AddMovieSearchResult {
|
||||
tmdb_id: 1234,
|
||||
title: HorizontallyScrollableText::from("Test"),
|
||||
original_language: language(),
|
||||
status: "released".to_owned(),
|
||||
overview: "New movie blah blah blah".to_owned(),
|
||||
genres: genres(),
|
||||
year: 2023,
|
||||
runtime: 120,
|
||||
ratings: ratings_list(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movie_history_item() -> MovieHistoryItem {
|
||||
MovieHistoryItem {
|
||||
source_title: HorizontallyScrollableText::from("Test"),
|
||||
quality: quality_wrapper(),
|
||||
languages: vec![language()],
|
||||
date: DateTime::from(DateTime::parse_from_rfc3339("2022-12-30T07:37:56Z").unwrap()),
|
||||
event_type: "grabbed".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_record() -> DownloadRecord {
|
||||
DownloadRecord {
|
||||
title: "Test Download Title".to_owned(),
|
||||
status: "downloading".to_owned(),
|
||||
id: 1,
|
||||
movie_id: 1,
|
||||
size: 3543348019,
|
||||
sizeleft: 1771674009,
|
||||
output_path: Some(HorizontallyScrollableText::from("/nfs/movies/Test")),
|
||||
indexer: "kickass torrents".to_owned(),
|
||||
download_client: "transmission".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downloads_response() -> DownloadsResponse {
|
||||
DownloadsResponse {
|
||||
records: vec![download_record()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_folder() -> RootFolder {
|
||||
RootFolder {
|
||||
id: 1,
|
||||
path: "/nfs".to_owned(),
|
||||
accessible: true,
|
||||
free_space: 219902325555200,
|
||||
unmapped_folders: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cast_credit() -> Credit {
|
||||
Credit {
|
||||
person_name: "Madison Clarke".to_owned(),
|
||||
character: Some("Johnny Blaze".to_owned()),
|
||||
department: None,
|
||||
job: None,
|
||||
credit_type: CreditType::Cast,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crew_credit() -> Credit {
|
||||
Credit {
|
||||
person_name: "Alex Clarke".to_owned(),
|
||||
character: None,
|
||||
department: Some("Music".to_owned()),
|
||||
job: Some("Composition".to_owned()),
|
||||
credit_type: CreditType::Crew,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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")),
|
||||
},
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indexer_settings() -> IndexerSettings {
|
||||
IndexerSettings {
|
||||
rss_sync_interval: 60,
|
||||
allow_hardcoded_subs: true,
|
||||
id: 1,
|
||||
..IndexerSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::*;
|
||||
use crate::models::radarr_models::{
|
||||
EditCollectionParams, EditMovieParams, IndexerSettings, RadarrTaskName,
|
||||
};
|
||||
use crate::models::servarr_data::radarr::modals::EditMovieModal;
|
||||
use crate::models::servarr_models::EditIndexerParams;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::App;
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_movie(
|
||||
#[values(
|
||||
RadarrEvent::AddMovie(AddMovieBody::default()),
|
||||
RadarrEvent::EditMovie(EditMovieParams::default()),
|
||||
RadarrEvent::GetMovies,
|
||||
RadarrEvent::GetMovieDetails(0),
|
||||
RadarrEvent::DeleteMovie(DeleteMovieParams::default()),
|
||||
RadarrEvent::ToggleMovieMonitoring(0)
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/movie");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_collection(
|
||||
#[values(
|
||||
RadarrEvent::GetCollections,
|
||||
RadarrEvent::EditCollection(EditCollectionParams::default())
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/collection");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_indexer(
|
||||
#[values(
|
||||
RadarrEvent::GetIndexers,
|
||||
RadarrEvent::DeleteIndexer(0),
|
||||
RadarrEvent::EditIndexer(EditIndexerParams::default())
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/indexer");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_all_indexer_settings(
|
||||
#[values(
|
||||
RadarrEvent::GetAllIndexerSettings,
|
||||
RadarrEvent::EditAllIndexerSettings(IndexerSettings::default())
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/config/indexer");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_root_folder(
|
||||
#[values(
|
||||
RadarrEvent::AddRootFolder(AddRootFolderBody::default()),
|
||||
RadarrEvent::GetRootFolders,
|
||||
RadarrEvent::DeleteRootFolder(0)
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/rootfolder");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_tag(
|
||||
#[values(
|
||||
RadarrEvent::AddTag(String::new()),
|
||||
RadarrEvent::GetTags,
|
||||
RadarrEvent::DeleteTag(0)
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/tag");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_release(
|
||||
#[values(
|
||||
RadarrEvent::GetReleases(0),
|
||||
RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default())
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/release");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_queue(
|
||||
#[values(RadarrEvent::GetDownloads(0), RadarrEvent::DeleteDownload(0))] event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/queue");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_host_config(
|
||||
#[values(RadarrEvent::GetHostConfig, RadarrEvent::GetSecurityConfig)] event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/config/host");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_command(
|
||||
#[values(
|
||||
RadarrEvent::StartTask(RadarrTaskName::default()),
|
||||
RadarrEvent::GetQueuedEvents,
|
||||
RadarrEvent::TriggerAutomaticSearch(0),
|
||||
RadarrEvent::UpdateAndScan(0),
|
||||
RadarrEvent::UpdateAllMovies,
|
||||
RadarrEvent::UpdateDownloads,
|
||||
RadarrEvent::UpdateCollections
|
||||
)]
|
||||
event: RadarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/command");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(RadarrEvent::ClearBlocklist, "/blocklist/bulk")]
|
||||
#[case(RadarrEvent::DeleteBlocklistItem(1), "/blocklist")]
|
||||
#[case(RadarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
|
||||
#[case(RadarrEvent::GetLogs(500), "/log")]
|
||||
#[case(RadarrEvent::SearchNewMovie(String::new()), "/movie/lookup")]
|
||||
#[case(RadarrEvent::GetMovieCredits(0), "/credit")]
|
||||
#[case(RadarrEvent::GetMovieHistory(0), "/history/movie")]
|
||||
#[case(RadarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(RadarrEvent::GetQualityProfiles, "/qualityprofile")]
|
||||
#[case(RadarrEvent::GetStatus, "/system/status")]
|
||||
#[case(RadarrEvent::GetTasks, "/system/task")]
|
||||
#[case(RadarrEvent::GetUpdates, "/update")]
|
||||
#[case(RadarrEvent::TestIndexer(0), "/indexer/test")]
|
||||
#[case(RadarrEvent::TestAllIndexers, "/indexer/testall")]
|
||||
#[case(RadarrEvent::HealthCheck, "/health")]
|
||||
fn test_resource(#[case] event: RadarrEvent, #[case] expected_uri: String) {
|
||||
assert_str_eq!(event.resource(), expected_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_radarr_event() {
|
||||
assert_eq!(
|
||||
NetworkEvent::Radarr(RadarrEvent::HealthCheck),
|
||||
NetworkEvent::from(RadarrEvent::HealthCheck)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_healthcheck_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::HealthCheck,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
let _ = network.handle_radarr_event(RadarrEvent::HealthCheck).await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_quality_profiles_event() {
|
||||
let quality_profile_json = json!([{
|
||||
"id": 2222,
|
||||
"name": "HD - 1080p"
|
||||
}]);
|
||||
let response: Vec<QualityProfile> =
|
||||
serde_json::from_value(quality_profile_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(quality_profile_json),
|
||||
None,
|
||||
RadarrEvent::GetQualityProfiles,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::QualityProfiles(quality_profiles) = network
|
||||
.handle_radarr_event(RadarrEvent::GetQualityProfiles)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.quality_profile_map,
|
||||
BiMap::from_iter([(2222i64, "HD - 1080p".to_owned())])
|
||||
);
|
||||
assert_eq!(quality_profiles, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_tags_event() {
|
||||
let tags_json = json!([{
|
||||
"id": 2222,
|
||||
"label": "usenet"
|
||||
}]);
|
||||
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(tags_json),
|
||||
None,
|
||||
RadarrEvent::GetTags,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Tags(tags) = network
|
||||
.handle_radarr_event(RadarrEvent::GetTags)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.tags_map,
|
||||
BiMap::from_iter([(2222i64, "usenet".to_owned())])
|
||||
);
|
||||
assert_eq!(tags, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_radarr_tag() {
|
||||
let tag_json = json!({ "id": 3, "label": "testing" });
|
||||
let response: Tag = serde_json::from_value(tag_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": "testing" })),
|
||||
Some(tag_json),
|
||||
None,
|
||||
RadarrEvent::AddTag(String::new()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Tag(tag) = network
|
||||
.handle_radarr_event(RadarrEvent::AddTag("testing".to_owned()))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.tags_map,
|
||||
BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "testing".to_owned())
|
||||
])
|
||||
);
|
||||
assert_eq!(tag, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_radarr_tag_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::DeleteTag(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::DeleteTag(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_and_add_radarr_tag_ids_vec() {
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let tags = " test,HI ,, usenet ";
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.data.radarr_data.tags_map = BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "hi".to_owned()),
|
||||
]);
|
||||
}
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert_eq!(
|
||||
network.extract_and_add_radarr_tag_ids_vec(tags).await,
|
||||
vec![2, 3, 1]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_and_add_radarr_tag_ids_vec_add_missing_tags_first() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": "TESTING" })),
|
||||
Some(json!({ "id": 3, "label": "testing" })),
|
||||
None,
|
||||
RadarrEvent::GetTags,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let tags = "usenet, test, TESTING";
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.data.radarr_data.edit_movie_modal = Some(EditMovieModal {
|
||||
tags: tags.into(),
|
||||
..EditMovieModal::default()
|
||||
});
|
||||
app.data.radarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
|
||||
}
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
let tag_ids_vec = network.extract_and_add_radarr_tag_ids_vec(tags).await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(tag_ids_vec, vec![1, 2, 3]);
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.tags_map,
|
||||
BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "testing".to_owned())
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_root_folders_network_tests.rs"]
|
||||
mod radarr_root_folders_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn add_radarr_root_folder(
|
||||
&mut self,
|
||||
add_root_folder_body: AddRootFolderBody,
|
||||
) -> Result<Value> {
|
||||
info!("Adding new root folder to Radarr");
|
||||
let event = RadarrEvent::AddRootFolder(AddRootFolderBody::default());
|
||||
|
||||
debug!("Add root folder body: {add_root_folder_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(add_root_folder_body),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<AddRootFolderBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn delete_radarr_root_folder(
|
||||
&mut self,
|
||||
root_folder_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = RadarrEvent::DeleteRootFolder(root_folder_id);
|
||||
info!("Deleting Radarr root folder for folder with id: {root_folder_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{root_folder_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Radarr root folders");
|
||||
let event = RadarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.radarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::RadarrSerdeable;
|
||||
use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::radarr_network_test_utils::test_utils::root_folder;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_radarr_root_folder_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"path": "/nfs/test"
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
RadarrEvent::AddRootFolder(AddRootFolderBody::default()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let add_root_folder_body = AddRootFolderBody {
|
||||
path: "/nfs/test".to_owned(),
|
||||
};
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::AddRootFolder(add_root_folder_body))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_radarr_root_folder_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
RadarrEvent::DeleteRootFolder(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_radarr_event(RadarrEvent::DeleteRootFolder(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_root_folders_event() {
|
||||
let root_folder_json = json!([{
|
||||
"id": 1,
|
||||
"path": "/nfs",
|
||||
"accessible": true,
|
||||
"freeSpace": 219902325555200u64,
|
||||
}]);
|
||||
let response: Vec<RootFolder> = serde_json::from_value(root_folder_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(root_folder_json),
|
||||
None,
|
||||
RadarrEvent::GetRootFolders,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::RootFolders(root_folders) = network
|
||||
.handle_radarr_event(RadarrEvent::GetRootFolders)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.root_folders.items,
|
||||
vec![root_folder()]
|
||||
);
|
||||
assert_eq!(root_folders, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
use crate::models::radarr_models::{RadarrTask, RadarrTaskName, SystemStatus};
|
||||
use crate::models::servarr_models::{
|
||||
CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
|
||||
};
|
||||
use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText};
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use indoc::formatdoc;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "radarr_system_network_tests.rs"]
|
||||
mod radarr_system_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_diskspace(
|
||||
&mut self,
|
||||
) -> Result<Vec<DiskSpace>> {
|
||||
info!("Fetching Radarr disk space");
|
||||
let event = RadarrEvent::GetDiskSpace;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<DiskSpace>>(request_props, |disk_space_vec, mut app| {
|
||||
app.data.radarr_data.disk_space_vec = disk_space_vec;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_host_config(
|
||||
&mut self,
|
||||
) -> Result<HostConfig> {
|
||||
info!("Fetching Radarr host config");
|
||||
let event = RadarrEvent::GetHostConfig;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), HostConfig>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_logs(
|
||||
&mut self,
|
||||
events: u64,
|
||||
) -> Result<LogResponse> {
|
||||
info!("Fetching Radarr logs");
|
||||
let event = RadarrEvent::GetLogs(events);
|
||||
|
||||
let params = format!("pageSize={events}&sortDirection=descending&sortKey=time");
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), LogResponse>(request_props, |log_response, mut app| {
|
||||
let mut logs = log_response.records;
|
||||
logs.reverse();
|
||||
|
||||
let log_lines = logs
|
||||
.into_iter()
|
||||
.map(|log| {
|
||||
if log.exception.is_some() {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log.logger.as_ref().unwrap(),
|
||||
log.exception_type.as_ref().unwrap(),
|
||||
log.exception.as_ref().unwrap()
|
||||
))
|
||||
} else {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log.logger.as_ref().unwrap(),
|
||||
log.message.as_ref().unwrap()
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
app.data.radarr_data.logs.set_items(log_lines);
|
||||
app.data.radarr_data.logs.scroll_to_bottom();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_queued_radarr_events(
|
||||
&mut self,
|
||||
) -> Result<Vec<QueueEvent>> {
|
||||
info!("Fetching Radarr queued events");
|
||||
let event = RadarrEvent::GetQueuedEvents;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QueueEvent>>(request_props, |queued_events_vec, mut app| {
|
||||
app
|
||||
.data
|
||||
.radarr_data
|
||||
.queued_events
|
||||
.set_items(queued_events_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_security_config(
|
||||
&mut self,
|
||||
) -> Result<SecurityConfig> {
|
||||
info!("Fetching Radarr security config");
|
||||
let event = RadarrEvent::GetSecurityConfig;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_status(
|
||||
&mut self,
|
||||
) -> Result<SystemStatus> {
|
||||
info!("Fetching Radarr system status");
|
||||
let event = RadarrEvent::GetStatus;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), SystemStatus>(request_props, |system_status, mut app| {
|
||||
app.data.radarr_data.version = system_status.version;
|
||||
app.data.radarr_data.start_time = system_status.start_time;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_updates(
|
||||
&mut self,
|
||||
) -> Result<Vec<Update>> {
|
||||
info!("Fetching Radarr updates");
|
||||
let event = RadarrEvent::GetUpdates;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Update>>(request_props, |updates_vec, mut app| {
|
||||
let latest_installed = if updates_vec
|
||||
.iter()
|
||||
.any(|update| update.latest && update.installed_on.is_some())
|
||||
{
|
||||
"already".to_owned()
|
||||
} else {
|
||||
"not".to_owned()
|
||||
};
|
||||
let updates = updates_vec
|
||||
.into_iter()
|
||||
.map(|update| {
|
||||
let install_status = if update.installed_on.is_some() {
|
||||
if update.installed {
|
||||
"(Currently Installed)".to_owned()
|
||||
} else {
|
||||
"(Previously Installed)".to_owned()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let vec_to_bullet_points = |vec: Vec<String>| {
|
||||
vec
|
||||
.iter()
|
||||
.map(|change| format!(" * {change}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
let mut update_info = formatdoc!(
|
||||
"{} - {} {install_status}
|
||||
{}",
|
||||
update.version,
|
||||
update.release_date,
|
||||
"-".repeat(200)
|
||||
);
|
||||
|
||||
if let Some(new_changes) = update.changes.new {
|
||||
let changes = vec_to_bullet_points(new_changes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
New:
|
||||
{changes}"
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(fixes) = update.changes.fixed {
|
||||
let fixes = vec_to_bullet_points(fixes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
Fixed:
|
||||
{fixes}"
|
||||
);
|
||||
}
|
||||
|
||||
update_info
|
||||
})
|
||||
.reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}"))
|
||||
.unwrap();
|
||||
|
||||
app.data.radarr_data.updates = ScrollableText::with_string(formatdoc!(
|
||||
"The latest version of Radarr is {latest_installed} installed
|
||||
|
||||
{updates}"
|
||||
));
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn get_radarr_tasks(
|
||||
&mut self,
|
||||
) -> Result<Vec<RadarrTask>> {
|
||||
info!("Fetching Radarr tasks");
|
||||
let event = RadarrEvent::GetTasks;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RadarrTask>>(request_props, |tasks_vec, mut app| {
|
||||
app.data.radarr_data.tasks.set_items(tasks_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::radarr_network) async fn start_radarr_task(
|
||||
&mut self,
|
||||
task_name: RadarrTaskName,
|
||||
) -> Result<Value> {
|
||||
let event = RadarrEvent::StartTask(task_name);
|
||||
|
||||
info!("Starting Radarr task: {task_name}");
|
||||
|
||||
let body = CommandBody {
|
||||
name: task_name.to_string(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::radarr_models::{RadarrSerdeable, RadarrTask, RadarrTaskName, SystemStatus};
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
|
||||
};
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::radarr_network::RadarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use chrono::DateTime;
|
||||
use indoc::formatdoc;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_diskspace_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(json!([
|
||||
{
|
||||
"freeSpace": 1111,
|
||||
"totalSpace": 2222,
|
||||
},
|
||||
{
|
||||
"freeSpace": 3333,
|
||||
"totalSpace": 4444
|
||||
}
|
||||
])),
|
||||
None,
|
||||
RadarrEvent::GetDiskSpace,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
let disk_space_vec = vec![
|
||||
DiskSpace {
|
||||
free_space: 1111,
|
||||
total_space: 2222,
|
||||
},
|
||||
DiskSpace {
|
||||
free_space: 3333,
|
||||
total_space: 4444,
|
||||
},
|
||||
];
|
||||
|
||||
if let RadarrSerdeable::DiskSpaces(disk_space) = network
|
||||
.handle_radarr_event(RadarrEvent::GetDiskSpace)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.disk_space_vec,
|
||||
disk_space_vec
|
||||
);
|
||||
assert_eq!(disk_space, disk_space_vec);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_host_config_event() {
|
||||
let host_config_response = json!({
|
||||
"bindAddress": "*",
|
||||
"port": 7878,
|
||||
"urlBase": "some.test.site/radarr",
|
||||
"instanceName": "Radarr",
|
||||
"applicationUrl": "https://some.test.site:7878/radarr",
|
||||
"enableSsl": true,
|
||||
"sslPort": 9898,
|
||||
"sslCertPath": "/app/radarr.pfx",
|
||||
"sslCertPassword": "test"
|
||||
});
|
||||
let response: HostConfig = serde_json::from_value(host_config_response.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(host_config_response),
|
||||
None,
|
||||
RadarrEvent::GetHostConfig,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::HostConfig(host_config) = network
|
||||
.handle_radarr_event(RadarrEvent::GetHostConfig)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(host_config, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_logs_event() {
|
||||
let expected_logs = vec![
|
||||
HorizontallyScrollableText::from(
|
||||
"2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception",
|
||||
),
|
||||
HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"),
|
||||
];
|
||||
let logs_response_json = json!({
|
||||
"page": 1,
|
||||
"pageSize": 500,
|
||||
"sortKey": "time",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 2,
|
||||
"records": [
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "info",
|
||||
"logger": "TestLogger",
|
||||
"message": "test message",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "fatal",
|
||||
"logger": "RadarrError",
|
||||
"exception": "test exception",
|
||||
"exceptionType": "Some.Big.Bad.Exception",
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
});
|
||||
let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(logs_response_json),
|
||||
None,
|
||||
RadarrEvent::GetLogs(500),
|
||||
None,
|
||||
Some("pageSize=500&sortDirection=descending&sortKey=time"),
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::LogResponse(logs) = network
|
||||
.handle_radarr_event(RadarrEvent::GetLogs(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.logs.items,
|
||||
expected_logs
|
||||
);
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.radarr_data
|
||||
.logs
|
||||
.current_selection()
|
||||
.text
|
||||
.contains("INFO"));
|
||||
assert_eq!(logs, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_queued_radarr_events_event() {
|
||||
let queued_events_json = json!([{
|
||||
"name": "RefreshMonitoredDownloads",
|
||||
"commandName": "Refresh Monitored Downloads",
|
||||
"status": "completed",
|
||||
"queued": "2023-05-20T21:29:16Z",
|
||||
"started": "2023-05-20T21:29:16Z",
|
||||
"ended": "2023-05-20T21:29:16Z",
|
||||
"duration": "00:00:00.5111547",
|
||||
"trigger": "scheduled",
|
||||
}]);
|
||||
let response: Vec<QueueEvent> = serde_json::from_value(queued_events_json.clone()).unwrap();
|
||||
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
|
||||
let expected_event = QueueEvent {
|
||||
name: "RefreshMonitoredDownloads".to_owned(),
|
||||
command_name: "Refresh Monitored Downloads".to_owned(),
|
||||
status: "completed".to_owned(),
|
||||
queued: timestamp,
|
||||
started: Some(timestamp),
|
||||
ended: Some(timestamp),
|
||||
duration: Some("00:00:00.5111547".to_owned()),
|
||||
trigger: "scheduled".to_owned(),
|
||||
};
|
||||
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(queued_events_json),
|
||||
None,
|
||||
RadarrEvent::GetQueuedEvents,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::QueueEvents(events) = network
|
||||
.handle_radarr_event(RadarrEvent::GetQueuedEvents)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.queued_events.items,
|
||||
vec![expected_event]
|
||||
);
|
||||
assert_eq!(events, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_security_config_event() {
|
||||
let security_config_response = json!({
|
||||
"authenticationMethod": "forms",
|
||||
"authenticationRequired": "disabledForLocalAddresses",
|
||||
"username": "test",
|
||||
"password": "some password",
|
||||
"apiKey": "someApiKey12345",
|
||||
"certificateValidation": "disabledForLocalAddresses",
|
||||
});
|
||||
let response: SecurityConfig =
|
||||
serde_json::from_value(security_config_response.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(security_config_response),
|
||||
None,
|
||||
RadarrEvent::GetSecurityConfig,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::SecurityConfig(security_config) = network
|
||||
.handle_radarr_event(RadarrEvent::GetSecurityConfig)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(security_config, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_status_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(json!({
|
||||
"version": "v1",
|
||||
"startTime": "2023-02-25T20:16:43Z"
|
||||
})),
|
||||
None,
|
||||
RadarrEvent::GetStatus,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap());
|
||||
|
||||
if let RadarrSerdeable::SystemStatus(status) = network
|
||||
.handle_radarr_event(RadarrEvent::GetStatus)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_str_eq!(app_arc.lock().await.data.radarr_data.version, "v1");
|
||||
assert_eq!(app_arc.lock().await.data.radarr_data.start_time, date_time);
|
||||
assert_eq!(
|
||||
status,
|
||||
SystemStatus {
|
||||
version: "v1".to_owned(),
|
||||
start_time: date_time
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_updates_event() {
|
||||
let updates_json = json!([{
|
||||
"version": "4.3.2.1",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": true,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": true,
|
||||
"changes": {
|
||||
"new": [
|
||||
"Cool new thing"
|
||||
],
|
||||
"fixed": [
|
||||
"Some bugs killed"
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"version": "3.2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": false,
|
||||
"changes": {
|
||||
"new": [
|
||||
"Cool new thing (old)",
|
||||
"Other cool new thing (old)"
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"version": "2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"latest": false,
|
||||
"changes": {
|
||||
"fixed": [
|
||||
"Killed bug 1",
|
||||
"Fixed bug 2"
|
||||
]
|
||||
},
|
||||
}]);
|
||||
let response: Vec<Update> = serde_json::from_value(updates_json.clone()).unwrap();
|
||||
let line_break = "-".repeat(200);
|
||||
let expected_text = ScrollableText::with_string(formatdoc!(
|
||||
"
|
||||
The latest version of Radarr is already installed
|
||||
|
||||
4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing
|
||||
Fixed:
|
||||
* Some bugs killed
|
||||
|
||||
|
||||
3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing (old)
|
||||
* Other cool new thing (old)
|
||||
|
||||
|
||||
2.1.0 - 2023-04-15 02:02:53 UTC
|
||||
{line_break}
|
||||
Fixed:
|
||||
* Killed bug 1
|
||||
* Fixed bug 2"
|
||||
));
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(updates_json),
|
||||
None,
|
||||
RadarrEvent::GetUpdates,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Updates(updates) = network
|
||||
.handle_radarr_event(RadarrEvent::GetUpdates)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_str_eq!(
|
||||
app_arc.lock().await.data.radarr_data.updates.get_text(),
|
||||
expected_text.get_text()
|
||||
);
|
||||
assert_eq!(updates, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_radarr_tasks_event() {
|
||||
let tasks_json = json!([{
|
||||
"name": "Application Check Update",
|
||||
"taskName": "ApplicationCheckUpdate",
|
||||
"interval": 360,
|
||||
"lastExecution": "2023-05-20T21:29:16Z",
|
||||
"nextExecution": "2023-05-20T21:29:16Z",
|
||||
"lastDuration": "00:00:00.5111547",
|
||||
},
|
||||
{
|
||||
"name": "Backup",
|
||||
"taskName": "Backup",
|
||||
"interval": 10080,
|
||||
"lastExecution": "2023-05-20T21:29:16Z",
|
||||
"nextExecution": "2023-05-20T21:29:16Z",
|
||||
"lastDuration": "00:00:00.5111547",
|
||||
}]);
|
||||
let response: Vec<RadarrTask> = serde_json::from_value(tasks_json.clone()).unwrap();
|
||||
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
|
||||
let expected_tasks = vec![
|
||||
RadarrTask {
|
||||
name: "Application Check Update".to_owned(),
|
||||
task_name: RadarrTaskName::ApplicationCheckUpdate,
|
||||
interval: 360,
|
||||
last_execution: timestamp,
|
||||
next_execution: timestamp,
|
||||
last_duration: "00:00:00.5111547".to_owned(),
|
||||
},
|
||||
RadarrTask {
|
||||
name: "Backup".to_owned(),
|
||||
task_name: RadarrTaskName::Backup,
|
||||
interval: 10080,
|
||||
last_execution: timestamp,
|
||||
next_execution: timestamp,
|
||||
last_duration: "00:00:00.5111547".to_owned(),
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(tasks_json),
|
||||
None,
|
||||
RadarrEvent::GetTasks,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Tasks(tasks) = network
|
||||
.handle_radarr_event(RadarrEvent::GetTasks)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.radarr_data.tasks.items,
|
||||
expected_tasks
|
||||
);
|
||||
assert_eq!(tasks, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_start_radarr_task_event() {
|
||||
let response = json!({ "test": "test"});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "ApplicationCheckUpdate"
|
||||
})),
|
||||
Some(response.clone()),
|
||||
None,
|
||||
RadarrEvent::StartTask(RadarrTaskName::ApplicationCheckUpdate),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let RadarrSerdeable::Value(value) = network
|
||||
.handle_radarr_event(RadarrEvent::StartTask(
|
||||
RadarrTaskName::ApplicationCheckUpdate,
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(value, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,112 @@
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::sonarr_models::{BlocklistItem, BlocklistResponse};
|
||||
use crate::models::Route;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_blocklist_network_tests.rs"]
|
||||
mod sonarr_blocklist_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn clear_sonarr_blocklist(&mut self) -> Result<()> {
|
||||
info!("Clearing Sonarr blocklist");
|
||||
let event = SonarrEvent::ClearBlocklist;
|
||||
|
||||
let ids = self
|
||||
.app
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.blocklist
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.id)
|
||||
.collect::<Vec<i64>>();
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
Some(json!({"ids": ids})),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn delete_sonarr_blocklist_item(
|
||||
&mut self,
|
||||
blocklist_item_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::DeleteBlocklistItem(blocklist_item_id);
|
||||
info!("Deleting Sonarr blocklist item for item with id: {blocklist_item_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{blocklist_item_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_blocklist(
|
||||
&mut self,
|
||||
) -> Result<BlocklistResponse> {
|
||||
info!("Fetching Sonarr blocklist");
|
||||
let event = SonarrEvent::GetBlocklist;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), BlocklistResponse>(request_props, |blocklist_resp, mut app| {
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Sonarr(ActiveSonarrBlock::BlocklistSortPrompt, _)
|
||||
) {
|
||||
let mut blocklist_vec: Vec<BlocklistItem> = blocklist_resp
|
||||
.records
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
if let Some(series) = app
|
||||
.data
|
||||
.sonarr_data
|
||||
.series
|
||||
.items
|
||||
.iter()
|
||||
.find(|it| it.id == item.series_id)
|
||||
{
|
||||
BlocklistItem {
|
||||
series_title: Some(series.title.text.clone()),
|
||||
..item
|
||||
}
|
||||
} else {
|
||||
item
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
blocklist_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.sonarr_data.blocklist.set_items(blocklist_vec);
|
||||
app.data.sonarr_data.blocklist.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::sonarr_models::{BlocklistItem, BlocklistResponse, Series, SonarrSerdeable};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
blocklist_item, series,
|
||||
};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::{json, Number};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_clear_sonarr_blocklist_event() {
|
||||
let blocklist_items = vec![
|
||||
BlocklistItem {
|
||||
id: 1,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 2,
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 3,
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let expected_request_json = json!({ "ids": [1, 2, 3]});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
Some(expected_request_json),
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::ClearBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.blocklist
|
||||
.set_items(blocklist_items);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::ClearBlocklist)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_sonarr_blocklist_item_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::DeleteBlocklistItem(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.blocklist
|
||||
.set_items(vec![blocklist_item()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DeleteBlocklistItem(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_blocklist_event(#[values(true, false)] use_custom_sorting: bool) {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"seriesId": 1007,
|
||||
"episodeIds": [42020],
|
||||
"sourceTitle": "z series",
|
||||
"languages": [{ "id": 1, "name": "English" }],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 123
|
||||
},
|
||||
{
|
||||
"seriesId": 2001,
|
||||
"episodeIds": [42018],
|
||||
"sourceTitle": "A Series",
|
||||
"languages": [{ "id": 1, "name": "English" }],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 456
|
||||
}]});
|
||||
let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap();
|
||||
let mut expected_blocklist = vec![
|
||||
BlocklistItem {
|
||||
id: 123,
|
||||
series_id: 1007,
|
||||
series_title: Some("Z Series".into()),
|
||||
source_title: "z series".into(),
|
||||
episode_ids: vec![Number::from(42020)],
|
||||
..blocklist_item()
|
||||
},
|
||||
BlocklistItem {
|
||||
id: 456,
|
||||
series_id: 2001,
|
||||
source_title: "A Series".into(),
|
||||
episode_ids: vec![Number::from(42018)],
|
||||
..blocklist_item()
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(blocklist_json),
|
||||
None,
|
||||
SonarrEvent::GetBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.series
|
||||
.set_items(vec![Series {
|
||||
id: 1007,
|
||||
title: "Z Series".into(),
|
||||
..series()
|
||||
}]);
|
||||
app_arc.lock().await.data.sonarr_data.blocklist.sort_asc = true;
|
||||
if use_custom_sorting {
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
expected_blocklist.sort_by(cmp_fn);
|
||||
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
}
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::BlocklistResponse(blocklist) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetBlocklist)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.blocklist.items,
|
||||
expected_blocklist
|
||||
);
|
||||
assert!(app_arc.lock().await.data.sonarr_data.blocklist.sort_asc);
|
||||
assert_eq!(blocklist, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_blocklist_event_no_op_when_user_is_selecting_sort_options() {
|
||||
let blocklist_json = json!({"records": [{
|
||||
"seriesId": 1007,
|
||||
"episodeIds": [42020],
|
||||
"sourceTitle": "z series",
|
||||
"languages": [{ "id": 1, "name": "English" }],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 123
|
||||
},
|
||||
{
|
||||
"seriesId": 2001,
|
||||
"episodeIds": [42018],
|
||||
"sourceTitle": "A Series",
|
||||
"languages": [{ "id": 1, "name": "English" }],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"date": "2024-02-10T07:28:45Z",
|
||||
"protocol": "usenet",
|
||||
"indexer": "NZBgeek (Prowlarr)",
|
||||
"message": "test message",
|
||||
"id": 456
|
||||
}]});
|
||||
let response: BlocklistResponse = serde_json::from_value(blocklist_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(blocklist_json),
|
||||
None,
|
||||
SonarrEvent::GetBlocklist,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.blocklist.sort_asc = true;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.push_navigation_stack(ActiveSonarrBlock::BlocklistSortPrompt.into());
|
||||
let cmp_fn = |a: &BlocklistItem, b: &BlocklistItem| {
|
||||
a.source_title
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.to_lowercase())
|
||||
};
|
||||
let blocklist_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.blocklist
|
||||
.sorting(vec![blocklist_sort_option]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::BlocklistResponse(blocklist) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetBlocklist)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc.lock().await.data.sonarr_data.blocklist.is_empty());
|
||||
assert!(app_arc.lock().await.data.sonarr_data.blocklist.sort_asc);
|
||||
assert_eq!(blocklist, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use crate::models::servarr_models::CommandBody;
|
||||
use crate::models::sonarr_models::DownloadsResponse;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_downloads_network_tests.rs"]
|
||||
mod sonarr_downloads_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn delete_sonarr_download(
|
||||
&mut self,
|
||||
download_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::DeleteDownload(download_id);
|
||||
info!("Deleting Sonarr download for download with id: {download_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{download_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_downloads(
|
||||
&mut self,
|
||||
count: u64,
|
||||
) -> Result<DownloadsResponse> {
|
||||
info!("Fetching Sonarr downloads");
|
||||
let event = SonarrEvent::GetDownloads(count);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("pageSize={count}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), DownloadsResponse>(request_props, |queue_response, mut app| {
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.downloads
|
||||
.set_items(queue_response.records);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn update_sonarr_downloads(
|
||||
&mut self,
|
||||
) -> Result<Value> {
|
||||
info!("Updating Sonarr downloads");
|
||||
let event = SonarrEvent::UpdateDownloads;
|
||||
let body = CommandBody {
|
||||
name: "RefreshMonitoredDownloads".to_owned(),
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::sonarr_models::{DownloadsResponse, SonarrSerdeable};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
download_record, downloads_response,
|
||||
};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_sonarr_download_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::DeleteDownload(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.downloads
|
||||
.set_items(vec![download_record()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DeleteDownload(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_downloads_event() {
|
||||
let downloads_response_json = json!({
|
||||
"records": [{
|
||||
"title": "Test Download Title",
|
||||
"status": "downloading",
|
||||
"id": 1,
|
||||
"episodeId": 1,
|
||||
"size": 3543348019f64,
|
||||
"sizeleft": 1771674009f64,
|
||||
"outputPath": "/nfs/tv/Test show/season 1/",
|
||||
"indexer": "kickass torrents",
|
||||
"downloadClient": "transmission",
|
||||
}]
|
||||
});
|
||||
let response: DownloadsResponse =
|
||||
serde_json::from_value(downloads_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(downloads_response_json),
|
||||
None,
|
||||
SonarrEvent::GetDownloads(500),
|
||||
None,
|
||||
Some("pageSize=500"),
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::DownloadsResponse(downloads) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetDownloads(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.downloads.items,
|
||||
downloads_response().records
|
||||
);
|
||||
assert_eq!(downloads, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_update_sonarr_downloads_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "RefreshMonitoredDownloads"
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
SonarrEvent::UpdateDownloads,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::UpdateDownloads)
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::sonarr_models::SonarrHistoryWrapper;
|
||||
use crate::models::Route;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_history_network_tests.rs"]
|
||||
mod sonarr_history_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_history(
|
||||
&mut self,
|
||||
events: u64,
|
||||
) -> Result<SonarrHistoryWrapper> {
|
||||
info!("Fetching all Sonarr history events");
|
||||
let event = SonarrEvent::GetHistory(events);
|
||||
|
||||
let params = format!("pageSize={events}&sortDirection=descending&sortKey=date");
|
||||
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 !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Sonarr(ActiveSonarrBlock::HistorySortPrompt, _)
|
||||
) {
|
||||
let mut history_vec = history_response.records;
|
||||
history_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
app.data.sonarr_data.history.set_items(history_vec);
|
||||
app.data.sonarr_data.history.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn mark_sonarr_history_item_as_failed(
|
||||
&mut self,
|
||||
history_item_id: i64,
|
||||
) -> Result<Value> {
|
||||
info!("Marking the Sonarr history item with ID: {history_item_id} as 'failed'");
|
||||
let event = SonarrEvent::MarkHistoryItemAsFailed(history_item_id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
None,
|
||||
Some(format!("/{history_item_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::sonarr_models::{SonarrHistoryItem, SonarrHistoryWrapper, SonarrSerdeable};
|
||||
use crate::models::stateful_table::SortOption;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::history_item;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_history_event(#[values(true, false)] use_custom_sorting: bool) {
|
||||
let history_json = json!({"records": [{
|
||||
"id": 123,
|
||||
"sourceTitle": "z episode",
|
||||
"episodeId": 1007,
|
||||
"quality": { "quality": { "name": "Bluray-1080p" } },
|
||||
"languages": [{ "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" } },
|
||||
"languages": [{ "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 mut 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::GetHistory(500),
|
||||
None,
|
||||
Some("pageSize=500&sortDirection=descending&sortKey=date"),
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.history.sort_asc = true;
|
||||
if use_custom_sorting {
|
||||
let cmp_fn = |a: &SonarrHistoryItem, b: &SonarrHistoryItem| {
|
||||
a.source_title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.text.to_lowercase())
|
||||
};
|
||||
expected_history_items.sort_by(cmp_fn);
|
||||
|
||||
let history_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.history
|
||||
.sorting(vec![history_sort_option]);
|
||||
}
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::SonarrHistoryWrapper(history) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetHistory(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.history.items,
|
||||
expected_history_items
|
||||
);
|
||||
assert!(app_arc.lock().await.data.sonarr_data.history.sort_asc);
|
||||
assert_eq!(history, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_history_event_no_op_when_user_is_selecting_sort_options() {
|
||||
let history_json = json!({"records": [{
|
||||
"id": 123,
|
||||
"sourceTitle": "z episode",
|
||||
"episodeId": 1007,
|
||||
"quality": { "quality": { "name": "Bluray-1080p" } },
|
||||
"languages": [{ "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" } },
|
||||
"languages": [{ "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 (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(history_json),
|
||||
None,
|
||||
SonarrEvent::GetHistory(500),
|
||||
None,
|
||||
Some("pageSize=500&sortDirection=descending&sortKey=date"),
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.history.sort_asc = true;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.push_navigation_stack(ActiveSonarrBlock::HistorySortPrompt.into());
|
||||
let cmp_fn = |a: &SonarrHistoryItem, b: &SonarrHistoryItem| {
|
||||
a.source_title
|
||||
.text
|
||||
.to_lowercase()
|
||||
.cmp(&b.source_title.text.to_lowercase())
|
||||
};
|
||||
let history_sort_option = SortOption {
|
||||
name: "Source Title",
|
||||
cmp_fn: Some(cmp_fn),
|
||||
};
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.history
|
||||
.sorting(vec![history_sort_option]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::SonarrHistoryWrapper(history) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetHistory(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc.lock().await.data.sonarr_data.history.is_empty());
|
||||
assert!(app_arc.lock().await.data.sonarr_data.history.sort_asc);
|
||||
assert_eq!(history, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_mark_sonarr_history_item_as_failed_event() {
|
||||
let expected_history_item_id = 1;
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
None,
|
||||
Some(json!({})),
|
||||
None,
|
||||
SonarrEvent::MarkHistoryItemAsFailed(expected_history_item_id),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::MarkHistoryItemAsFailed(
|
||||
expected_history_item_id
|
||||
))
|
||||
.await
|
||||
.is_ok());
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
|
||||
use crate::models::sonarr_models::IndexerSettings;
|
||||
use crate::models::stateful_table::StatefulTable;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_indexers_network_tests.rs"]
|
||||
mod sonarr_indexers_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn delete_sonarr_indexer(
|
||||
&mut self,
|
||||
indexer_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::DeleteIndexer(indexer_id);
|
||||
info!("Deleting Sonarr indexer for indexer with id: {indexer_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{indexer_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn edit_all_sonarr_indexer_settings(
|
||||
&mut self,
|
||||
params: IndexerSettings,
|
||||
) -> Result<Value> {
|
||||
info!("Updating Sonarr indexer settings");
|
||||
let event = SonarrEvent::EditAllIndexerSettings(IndexerSettings::default());
|
||||
debug!("Indexer settings body: {params:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Put, Some(params), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<IndexerSettings, Value>(request_props, |_, _| {})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn edit_sonarr_indexer(
|
||||
&mut self,
|
||||
mut edit_indexer_params: EditIndexerParams,
|
||||
) -> Result<()> {
|
||||
if let Some(tag_input_str) = edit_indexer_params.tag_input_string.as_ref() {
|
||||
let tag_ids_vec = self.extract_and_add_sonarr_tag_ids_vec(tag_input_str).await;
|
||||
edit_indexer_params.tags = Some(tag_ids_vec);
|
||||
}
|
||||
let detail_event = SonarrEvent::GetIndexers;
|
||||
let event = SonarrEvent::EditIndexer(EditIndexerParams::default());
|
||||
let id = edit_indexer_params.indexer_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)?;
|
||||
|
||||
let (
|
||||
name,
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags,
|
||||
priority,
|
||||
) = {
|
||||
let priority = detailed_indexer_body["priority"]
|
||||
.as_i64()
|
||||
.expect("Unable to deserialize 'priority'");
|
||||
let seed_ratio_field_option = detailed_indexer_body["fields"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|field| field["name"] == "seedCriteria.seedRatio");
|
||||
let name = edit_indexer_params.name.unwrap_or(
|
||||
detailed_indexer_body["name"]
|
||||
.as_str()
|
||||
.expect("Unable to deserialize 'name'")
|
||||
.to_owned(),
|
||||
);
|
||||
let enable_rss = edit_indexer_params.enable_rss.unwrap_or(
|
||||
detailed_indexer_body["enableRss"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableRss'"),
|
||||
);
|
||||
let enable_automatic_search = edit_indexer_params.enable_automatic_search.unwrap_or(
|
||||
detailed_indexer_body["enableAutomaticSearch"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableAutomaticSearch"),
|
||||
);
|
||||
let enable_interactive_search = edit_indexer_params.enable_interactive_search.unwrap_or(
|
||||
detailed_indexer_body["enableInteractiveSearch"]
|
||||
.as_bool()
|
||||
.expect("Unable to deserialize 'enableInteractiveSearch'"),
|
||||
);
|
||||
let url = edit_indexer_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 = edit_indexer_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 = edit_indexer_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 edit_indexer_params.clear_tags {
|
||||
vec![]
|
||||
} else {
|
||||
edit_indexer_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 = edit_indexer_params.priority.unwrap_or(priority);
|
||||
|
||||
(
|
||||
name,
|
||||
enable_rss,
|
||||
enable_automatic_search,
|
||||
enable_interactive_search,
|
||||
url,
|
||||
api_key,
|
||||
seed_ratio,
|
||||
tags,
|
||||
priority,
|
||||
)
|
||||
};
|
||||
|
||||
*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}")),
|
||||
Some("forceSave=true".to_owned()),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_all_sonarr_indexer_settings(
|
||||
&mut self,
|
||||
) -> Result<IndexerSettings> {
|
||||
info!("Fetching Sonarr indexer settings");
|
||||
let event = SonarrEvent::GetAllIndexerSettings;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), IndexerSettings>(request_props, |indexer_settings, mut app| {
|
||||
if app.data.sonarr_data.indexer_settings.is_none() {
|
||||
app.data.sonarr_data.indexer_settings = Some(indexer_settings);
|
||||
} else {
|
||||
debug!("Indexer Settings are being modified. Ignoring update...");
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_indexers(
|
||||
&mut self,
|
||||
) -> Result<Vec<Indexer>> {
|
||||
info!("Fetching Sonarr indexers");
|
||||
let event = SonarrEvent::GetIndexers;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Indexer>>(request_props, |indexers, mut app| {
|
||||
app.data.sonarr_data.indexers.set_items(indexers);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn test_sonarr_indexer(
|
||||
&mut self,
|
||||
indexer_id: i64,
|
||||
) -> Result<Value> {
|
||||
let detail_event = SonarrEvent::GetIndexers;
|
||||
let event = SonarrEvent::TestIndexer(indexer_id);
|
||||
info!("Testing Sonarr indexer with ID: {indexer_id}");
|
||||
|
||||
info!("Fetching indexer details for indexer with ID: {indexer_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{indexer_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut test_body: Value = Value::default();
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_indexer_body, _| {
|
||||
test_body = detailed_indexer_body;
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Testing indexer");
|
||||
|
||||
let mut request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(test_body), None, None)
|
||||
.await;
|
||||
request_props.ignore_status_code = true;
|
||||
|
||||
self
|
||||
.handle_request::<Value, Value>(request_props, |test_results, mut app| {
|
||||
if test_results.as_object().is_none() {
|
||||
app.data.sonarr_data.indexer_test_errors = Some(
|
||||
test_results.as_array().unwrap()[0]
|
||||
.get("errorMessage")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
} else {
|
||||
app.data.sonarr_data.indexer_test_errors = Some(String::new());
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn test_all_sonarr_indexers(
|
||||
&mut self,
|
||||
) -> Result<Vec<IndexerTestResult>> {
|
||||
info!("Testing all Sonarr indexers");
|
||||
let event = SonarrEvent::TestAllIndexers;
|
||||
|
||||
let mut request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, None, None, None)
|
||||
.await;
|
||||
request_props.ignore_status_code = true;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<IndexerTestResult>>(request_props, |test_results, mut app| {
|
||||
let mut test_all_indexer_results = StatefulTable::default();
|
||||
let indexers = app.data.sonarr_data.indexers.items.clone();
|
||||
let modal_test_results = test_results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
let name = indexers
|
||||
.iter()
|
||||
.filter(|&indexer| indexer.id == result.id)
|
||||
.map(|indexer| indexer.name.clone())
|
||||
.nth(0)
|
||||
.unwrap_or_default();
|
||||
let validation_failures = result
|
||||
.validation_failures
|
||||
.iter()
|
||||
.map(|failure| {
|
||||
format!(
|
||||
"Failure for field '{}': {}",
|
||||
failure.property_name, failure.error_message
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
IndexerTestResultModalItem {
|
||||
name: name.unwrap_or_default(),
|
||||
is_valid: result.is_valid,
|
||||
validation_failures: validation_failures.into(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
test_all_indexer_results.set_items(modal_test_results);
|
||||
app.data.sonarr_data.indexer_test_all_results = Some(test_all_indexer_results);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,930 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
||||
use crate::models::servarr_models::{EditIndexerParams, Indexer, IndexerTestResult};
|
||||
use crate::models::sonarr_models::SonarrSerdeable;
|
||||
use crate::models::HorizontallyScrollableText;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
indexer, indexer_settings,
|
||||
};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, NetworkResource, RequestMethod};
|
||||
use bimap::BiMap;
|
||||
use mockito::Matcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_sonarr_indexer_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::DeleteIndexer(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DeleteIndexer(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_all_indexer_settings_event() {
|
||||
let indexer_settings_json = json!({
|
||||
"id": 1,
|
||||
"minimumAge": 1,
|
||||
"maximumSize": 12345,
|
||||
"retention": 1,
|
||||
"rssSyncInterval": 60
|
||||
});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Put,
|
||||
Some(indexer_settings_json),
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::EditAllIndexerSettings(indexer_settings()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditAllIndexerSettings(indexer_settings()))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_edit_sonarr_indexer_event() {
|
||||
let expected_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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
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": 0,
|
||||
"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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_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_does_not_overwrite_tags_vec_if_tag_input_string_is_none(
|
||||
) {
|
||||
let expected_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(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
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": 0,
|
||||
"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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_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_does_not_add_seed_ratio_when_seed_ratio_field_is_none_in_details(
|
||||
) {
|
||||
let expected_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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
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": 0,
|
||||
"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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_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_populates_the_seed_ratio_value_when_seed_ratio_field_is_present_in_details(
|
||||
) {
|
||||
let expected_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()),
|
||||
tag_input_string: Some("usenet, testing".to_owned()),
|
||||
priority: Some(0),
|
||||
..EditIndexerParams::default()
|
||||
};
|
||||
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": 0,
|
||||
"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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(expected_edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_indexer_edit_body_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "testing".to_owned())]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(expected_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_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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(indexer_details_json))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(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_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?forceSave=true",
|
||||
SonarrEvent::EditIndexer(edit_indexer_params.clone()).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_edit_indexer_body))
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::EditIndexer(edit_indexer_params))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_details_server.assert_async().await;
|
||||
async_edit_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_indexers_event() {
|
||||
let indexers_response_json = json!([{
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"supportsRss": true,
|
||||
"supportsSearch": true,
|
||||
"protocol": "torrent",
|
||||
"priority": 25,
|
||||
"downloadClientId": 0,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"implementationName": "Torznab",
|
||||
"implementation": "Torznab",
|
||||
"configContract": "TorznabSettings",
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
}]);
|
||||
let response: Vec<Indexer> = serde_json::from_value(indexers_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(indexers_response_json),
|
||||
None,
|
||||
SonarrEvent::GetIndexers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Indexers(indexers) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetIndexers)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.indexers.items,
|
||||
vec![indexer()]
|
||||
);
|
||||
assert_eq!(indexers, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_sonarr_indexer_event_error() {
|
||||
let indexer_details_json = json!({
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
});
|
||||
let response_json = json!([
|
||||
{
|
||||
"isWarning": false,
|
||||
"propertyName": "",
|
||||
"errorMessage": "test failure",
|
||||
"severity": "error"
|
||||
}]);
|
||||
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_test_server = server
|
||||
.mock(
|
||||
"POST",
|
||||
format!("/api/v3{}", SonarrEvent::TestIndexer(1).resource()).as_str(),
|
||||
)
|
||||
.with_status(400)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(indexer_details_json.clone()))
|
||||
.with_body(response_json.to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Value(value) = network
|
||||
.handle_sonarr_event(SonarrEvent::TestIndexer(1))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_details_server.assert_async().await;
|
||||
async_test_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.indexer_test_errors,
|
||||
Some("\"test failure\"".to_owned())
|
||||
);
|
||||
assert_eq!(value, response_json)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_sonarr_indexer_event_success() {
|
||||
let indexer_details_json = json!({
|
||||
"enableRss": true,
|
||||
"enableAutomaticSearch": true,
|
||||
"enableInteractiveSearch": true,
|
||||
"name": "Test Indexer",
|
||||
"fields": [
|
||||
{
|
||||
"name": "baseUrl",
|
||||
"value": "https://test.com",
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "seedCriteria.seedRatio",
|
||||
"value": "1.2",
|
||||
},
|
||||
],
|
||||
"tags": [1],
|
||||
"id": 1
|
||||
});
|
||||
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_test_server = server
|
||||
.mock(
|
||||
"POST",
|
||||
format!("/api/v3{}", SonarrEvent::TestIndexer(1).resource()).as_str(),
|
||||
)
|
||||
.with_status(200)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(indexer_details_json.clone()))
|
||||
.with_body("{}")
|
||||
.create_async()
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexers
|
||||
.set_items(vec![indexer()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Value(value) = network
|
||||
.handle_sonarr_event(SonarrEvent::TestIndexer(1))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_details_server.assert_async().await;
|
||||
async_test_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.indexer_test_errors,
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(value, json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_test_all_sonarr_indexers_event() {
|
||||
let indexers = vec![
|
||||
Indexer {
|
||||
id: 1,
|
||||
name: Some("Test 1".to_owned()),
|
||||
..Indexer::default()
|
||||
},
|
||||
Indexer {
|
||||
id: 2,
|
||||
name: Some("Test 2".to_owned()),
|
||||
..Indexer::default()
|
||||
},
|
||||
];
|
||||
let indexer_test_results_modal_items = vec![
|
||||
IndexerTestResultModalItem {
|
||||
name: "Test 1".to_owned(),
|
||||
is_valid: true,
|
||||
validation_failures: HorizontallyScrollableText::default(),
|
||||
},
|
||||
IndexerTestResultModalItem {
|
||||
name: "Test 2".to_owned(),
|
||||
is_valid: false,
|
||||
validation_failures: "Failure for field 'test field 1': test error message, Failure for field 'test field 2': test error message 2".into(),
|
||||
},
|
||||
];
|
||||
let response_json = json!([
|
||||
{
|
||||
"id": 1,
|
||||
"isValid": true,
|
||||
"validationFailures": []
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"isValid": false,
|
||||
"validationFailures": [
|
||||
{
|
||||
"propertyName": "test field 1",
|
||||
"errorMessage": "test error message",
|
||||
"severity": "error"
|
||||
},
|
||||
{
|
||||
"propertyName": "test field 2",
|
||||
"errorMessage": "test error message 2",
|
||||
"severity": "error"
|
||||
},
|
||||
]
|
||||
}]);
|
||||
let response: Vec<IndexerTestResult> = serde_json::from_value(response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
None,
|
||||
Some(response_json),
|
||||
Some(400),
|
||||
SonarrEvent::TestAllIndexers,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexers
|
||||
.set_items(indexers);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::IndexerTestResults(results) = network
|
||||
.handle_sonarr_event(SonarrEvent::TestAllIndexers)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexer_test_all_results
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.indexer_test_all_results
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.items,
|
||||
indexer_test_results_modal_items
|
||||
);
|
||||
assert_eq!(results, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
use crate::models::servarr_data::sonarr::modals::{EpisodeDetailsModal, SeasonDetailsModal};
|
||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||
use crate::models::servarr_models::Language;
|
||||
use crate::models::sonarr_models::{
|
||||
DownloadRecord, DownloadStatus, Episode, EpisodeFile, MonitorEpisodeBody, SonarrCommandBody,
|
||||
SonarrHistoryWrapper, SonarrRelease,
|
||||
};
|
||||
use crate::models::{Route, ScrollableText};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use crate::utils::convert_to_gb;
|
||||
use anyhow::Result;
|
||||
use indoc::formatdoc;
|
||||
use log::info;
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_episodes_network_tests.rs"]
|
||||
mod sonarr_episodes_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn delete_sonarr_episode_file(
|
||||
&mut self,
|
||||
episode_file_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::DeleteEpisodeFile(episode_file_id);
|
||||
info!("Deleting Sonarr episode file for episode file with id: {episode_file_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{episode_file_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_episodes(
|
||||
&mut self,
|
||||
series_id: i64,
|
||||
) -> Result<Vec<Episode>> {
|
||||
let event = SonarrEvent::GetEpisodes(series_id);
|
||||
info!("Fetching episodes for Sonarr series with ID: {series_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("seriesId={series_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Episode>>(request_props, |mut episode_vec, mut app| {
|
||||
episode_vec.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
if !matches!(
|
||||
app.get_current_route(),
|
||||
Route::Sonarr(ActiveSonarrBlock::EpisodesSortPrompt, _)
|
||||
) {
|
||||
if app.data.sonarr_data.season_details_modal.is_none() {
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
}
|
||||
|
||||
let season_episodes_vec = if !app.data.sonarr_data.seasons.is_empty() {
|
||||
let season_number = app
|
||||
.data
|
||||
.sonarr_data
|
||||
.seasons
|
||||
.current_selection()
|
||||
.season_number;
|
||||
|
||||
episode_vec
|
||||
.into_iter()
|
||||
.filter(|episode| episode.season_number == season_number)
|
||||
.collect()
|
||||
} else {
|
||||
episode_vec
|
||||
};
|
||||
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episodes
|
||||
.set_items(season_episodes_vec);
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episodes
|
||||
.apply_sorting_toggle(false);
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_episode_files(
|
||||
&mut self,
|
||||
series_id: i64,
|
||||
) -> Result<Vec<EpisodeFile>> {
|
||||
let event = SonarrEvent::GetEpisodeFiles(series_id);
|
||||
info!("Fetching episodes files for Sonarr series with ID: {series_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("seriesId={series_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<EpisodeFile>>(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
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_episode_history(
|
||||
&mut self,
|
||||
episode_id: i64,
|
||||
) -> Result<SonarrHistoryWrapper> {
|
||||
info!("Fetching Sonarr history for episode with ID: {episode_id}");
|
||||
let event = SonarrEvent::GetEpisodeHistory(episode_id);
|
||||
|
||||
let params =
|
||||
format!("episodeId={episode_id}&pageSize=1000&sortDirection=descending&sortKey=date");
|
||||
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());
|
||||
}
|
||||
|
||||
if app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.episode_details_modal
|
||||
.is_none()
|
||||
{
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal = Some(EpisodeDetailsModal::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()
|
||||
.episode_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_history
|
||||
.set_items(history_vec);
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_history
|
||||
.apply_sorting_toggle(false);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_episode_details(
|
||||
&mut self,
|
||||
episode_id: i64,
|
||||
) -> Result<Episode> {
|
||||
info!("Fetching Sonarr episode details");
|
||||
let event = SonarrEvent::GetEpisodeDetails(episode_id);
|
||||
|
||||
info!("Fetching episode details for episode with ID: {episode_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{episode_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Episode>(request_props, |episode_response, mut app| {
|
||||
if app.cli_mode {
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
}
|
||||
|
||||
if app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.expect("Season details modal is empty")
|
||||
.episode_details_modal
|
||||
.is_none()
|
||||
{
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal = Some(EpisodeDetailsModal::default());
|
||||
}
|
||||
|
||||
let Episode {
|
||||
id,
|
||||
title,
|
||||
air_date_utc,
|
||||
overview,
|
||||
has_file,
|
||||
season_number,
|
||||
episode_number,
|
||||
episode_file,
|
||||
..
|
||||
} = episode_response;
|
||||
let status = get_episode_status(has_file, &app.data.sonarr_data.downloads.items, id);
|
||||
let air_date = if let Some(air_date) = air_date_utc {
|
||||
format!("{air_date}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let episode_details_modal = app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
episode_details_modal.episode_details = ScrollableText::with_string(formatdoc!(
|
||||
"
|
||||
Title: {}
|
||||
Season: {season_number}
|
||||
Episode Number: {episode_number}
|
||||
Air Date: {air_date}
|
||||
Status: {status}
|
||||
Description: {}",
|
||||
title,
|
||||
overview.unwrap_or_default(),
|
||||
));
|
||||
if let Some(file) = episode_file {
|
||||
let size = convert_to_gb(file.size);
|
||||
episode_details_modal.file_details = formatdoc!(
|
||||
"
|
||||
Relative Path: {}
|
||||
Absolute Path: {}
|
||||
Size: {size:.2} GB
|
||||
Language: {}
|
||||
Date Added: {}",
|
||||
file.relative_path,
|
||||
file.path,
|
||||
file.languages.first().unwrap_or(&Language::default()).name,
|
||||
file.date_added,
|
||||
);
|
||||
|
||||
if let Some(media_info) = file.media_info {
|
||||
episode_details_modal.audio_details = formatdoc!(
|
||||
"
|
||||
Bitrate: {}
|
||||
Channels: {:.1}
|
||||
Codec: {}
|
||||
Languages: {}
|
||||
Stream Count: {}",
|
||||
media_info.audio_bitrate,
|
||||
media_info.audio_channels.as_f64().unwrap(),
|
||||
media_info.audio_codec.unwrap_or_default(),
|
||||
media_info.audio_languages.unwrap_or_default(),
|
||||
media_info.audio_stream_count
|
||||
);
|
||||
|
||||
episode_details_modal.video_details = formatdoc!(
|
||||
"
|
||||
Bit Depth: {}
|
||||
Bitrate: {}
|
||||
Codec: {}
|
||||
FPS: {}
|
||||
Resolution: {}
|
||||
Scan Type: {}
|
||||
Runtime: {}
|
||||
Subtitles: {}",
|
||||
media_info.video_bit_depth,
|
||||
media_info.video_bitrate,
|
||||
media_info.video_codec.unwrap_or_default(),
|
||||
media_info.video_fps.as_f64().unwrap(),
|
||||
media_info.resolution,
|
||||
media_info.scan_type,
|
||||
media_info.run_time,
|
||||
media_info.subtitles.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_episode_releases(
|
||||
&mut self,
|
||||
episode_id: i64,
|
||||
) -> Result<Vec<SonarrRelease>> {
|
||||
let event = SonarrEvent::GetEpisodeReleases(episode_id);
|
||||
info!("Fetching releases for episode with ID: {episode_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("episodeId={episode_id}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<SonarrRelease>>(request_props, |release_vec, mut app| {
|
||||
if app.data.sonarr_data.season_details_modal.is_none() {
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
}
|
||||
|
||||
if app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal
|
||||
.is_none()
|
||||
{
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal = Some(EpisodeDetailsModal::default());
|
||||
}
|
||||
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.episode_releases
|
||||
.set_items(release_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn toggle_sonarr_episode_monitoring(
|
||||
&mut self,
|
||||
episode_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::ToggleEpisodeMonitoring(episode_id);
|
||||
let detail_event = SonarrEvent::GetEpisodeDetails(0);
|
||||
|
||||
let monitored = {
|
||||
info!("Fetching episode details for episode id: {episode_id}");
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
detail_event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
Some(format!("/{episode_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut monitored = false;
|
||||
|
||||
self
|
||||
.handle_request::<(), Value>(request_props, |detailed_episode_body, _| {
|
||||
monitored = detailed_episode_body
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
})
|
||||
.await?;
|
||||
|
||||
monitored
|
||||
};
|
||||
|
||||
info!("Toggling monitoring for episode id: {episode_id}");
|
||||
|
||||
let body = MonitorEpisodeBody {
|
||||
episode_ids: vec![episode_id],
|
||||
monitored: !monitored,
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Put, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<MonitorEpisodeBody, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn trigger_automatic_episode_search(
|
||||
&mut self,
|
||||
episode_id: i64,
|
||||
) -> Result<Value> {
|
||||
let event = SonarrEvent::TriggerAutomaticEpisodeSearch(episode_id);
|
||||
info!("Searching indexers for episode with ID: {episode_id}");
|
||||
|
||||
let body = SonarrCommandBody {
|
||||
name: "EpisodeSearch".to_owned(),
|
||||
episode_ids: Some(vec![episode_id]),
|
||||
..SonarrCommandBody::default()
|
||||
};
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<SonarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn get_episode_status(has_file: bool, downloads_vec: &[DownloadRecord], episode_id: i64) -> String {
|
||||
if !has_file {
|
||||
let default_episode_id = Number::from(-1i64);
|
||||
if let Some(download) = downloads_vec.iter().find(|&download| {
|
||||
download
|
||||
.episode_id
|
||||
.as_ref()
|
||||
.unwrap_or(&default_episode_id)
|
||||
.as_i64()
|
||||
.unwrap()
|
||||
== episode_id
|
||||
}) {
|
||||
if download.status == DownloadStatus::Downloading {
|
||||
return "Downloading".to_owned();
|
||||
}
|
||||
|
||||
if download.status == DownloadStatus::Completed {
|
||||
return "Awaiting Import".to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
return "Missing".to_owned();
|
||||
}
|
||||
|
||||
"Downloaded".to_owned()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
use crate::models::sonarr_models::SonarrReleaseDownloadBody;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
mod episodes;
|
||||
mod seasons;
|
||||
mod series;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_library_network_tests.rs"]
|
||||
mod sonarr_library_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn download_sonarr_release(
|
||||
&mut self,
|
||||
sonarr_release_download_body: SonarrReleaseDownloadBody,
|
||||
) -> Result<Value> {
|
||||
let event = SonarrEvent::DownloadRelease(SonarrReleaseDownloadBody::default());
|
||||
info!("Downloading Sonarr release with params: {sonarr_release_download_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(sonarr_release_download_body),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<SonarrReleaseDownloadBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
|
||||
use crate::models::sonarr_models::{SonarrCommandBody, SonarrHistoryItem, SonarrRelease};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_seasons_network_tests.rs"]
|
||||
mod sonarr_seasons_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn toggle_sonarr_season_monitoring(
|
||||
&mut self,
|
||||
series_id_season_number_tuple: (i64, i64),
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::ToggleSeasonMonitoring(series_id_season_number_tuple);
|
||||
let (series_id, season_number) = series_id_season_number_tuple;
|
||||
|
||||
let detail_event = SonarrEvent::GetSeriesDetails(series_id);
|
||||
info!("Toggling season monitoring for season {season_number} in 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 season monitoring body");
|
||||
|
||||
match serde_json::from_str::<Value>(&response) {
|
||||
Ok(mut detailed_series_body) => {
|
||||
let monitored = detailed_series_body
|
||||
.get("seasons")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|season| season["seasonNumber"] == season_number)
|
||||
.unwrap()
|
||||
.get("monitored")
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap();
|
||||
|
||||
*detailed_series_body
|
||||
.get_mut("seasons")
|
||||
.unwrap()
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.find(|season| season["seasonNumber"] == season_number)
|
||||
.unwrap()
|
||||
.get_mut("monitored")
|
||||
.unwrap() = json!(!monitored);
|
||||
|
||||
debug!("Toggle season 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::<Value, ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Request for detailed series body was interrupted");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_season_releases(
|
||||
&mut self,
|
||||
series_season_id_tuple: (i64, i64),
|
||||
) -> Result<Vec<SonarrRelease>> {
|
||||
let event = SonarrEvent::GetSeasonReleases(series_season_id_tuple);
|
||||
let (series_id, season_number) = series_season_id_tuple;
|
||||
info!("Fetching releases for series with ID: {series_id} and season number: {season_number}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Get,
|
||||
None::<()>,
|
||||
None,
|
||||
Some(format!("seriesId={series_id}&seasonNumber={season_number}")),
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<SonarrRelease>>(request_props, |release_vec, mut app| {
|
||||
if app.data.sonarr_data.season_details_modal.is_none() {
|
||||
app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default());
|
||||
}
|
||||
|
||||
let season_releases_vec = release_vec
|
||||
.into_iter()
|
||||
.filter(|release| release.full_season)
|
||||
.collect();
|
||||
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.season_releases
|
||||
.set_items(season_releases_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_season_history(
|
||||
&mut self,
|
||||
series_season_id_tuple: (i64, i64),
|
||||
) -> Result<Vec<SonarrHistoryItem>> {
|
||||
let event = SonarrEvent::GetSeasonHistory(series_season_id_tuple);
|
||||
let (series_id, season_number) = series_season_id_tuple;
|
||||
info!("Fetching history for series with ID: {series_id} and season number: {season_number}");
|
||||
|
||||
let params = format!("seriesId={series_id}&seasonNumber={season_number}",);
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<SonarrHistoryItem>>(request_props, |history_items, 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_items;
|
||||
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
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn trigger_automatic_season_search(
|
||||
&mut self,
|
||||
series_season_id_tuple: (i64, i64),
|
||||
) -> Result<Value> {
|
||||
let event = SonarrEvent::TriggerAutomaticSeasonSearch(series_season_id_tuple);
|
||||
let (series_id, season_number) = series_season_id_tuple;
|
||||
info!("Searching indexers for series with ID: {series_id} and season number: {season_number}");
|
||||
|
||||
let body = SonarrCommandBody {
|
||||
name: "SeasonSearch".to_owned(),
|
||||
season_number: Some(season_number),
|
||||
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::<SonarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_data::sonarr::modals::SeasonDetailsModal;
|
||||
use crate::models::sonarr_models::{SonarrHistoryItem, SonarrRelease, SonarrSerdeable};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::{
|
||||
history_item, release, season, series, SERIES_JSON,
|
||||
};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, NetworkResource, RequestMethod};
|
||||
use mockito::Matcher;
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::{json, Value};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_toggle_season_monitoring_event() {
|
||||
let mut expected_body: Value = serde_json::from_str(SERIES_JSON).unwrap();
|
||||
*expected_body
|
||||
.get_mut("seasons")
|
||||
.unwrap()
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.find(|season| season["seasonNumber"] == 1)
|
||||
.unwrap()
|
||||
.get_mut("monitored")
|
||||
.unwrap() = json!(false);
|
||||
|
||||
let (async_details_server, app_arc, mut server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(serde_json::from_str(SERIES_JSON).unwrap()),
|
||||
None,
|
||||
SonarrEvent::GetSeriesDetails(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let async_toggle_server = server
|
||||
.mock(
|
||||
"PUT",
|
||||
format!(
|
||||
"/api/v3{}/1",
|
||||
SonarrEvent::ToggleSeasonMonitoring((1, 1)).resource()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.with_status(202)
|
||||
.match_header("X-Api-Key", "test1234")
|
||||
.match_body(Matcher::Json(expected_body))
|
||||
.create_async()
|
||||
.await;
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.data.sonarr_data.series.set_items(vec![series()]);
|
||||
app.data.sonarr_data.seasons.set_items(vec![season()]);
|
||||
}
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::ToggleSeasonMonitoring((1, 1)))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_details_server.assert_async().await;
|
||||
async_toggle_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_season_releases_event() {
|
||||
let release_json = json!([
|
||||
{
|
||||
"guid": "1234",
|
||||
"protocol": "torrent",
|
||||
"age": 1,
|
||||
"title": "Test Release",
|
||||
"indexer": "kickass torrents",
|
||||
"indexerId": 2,
|
||||
"size": 1234,
|
||||
"rejected": true,
|
||||
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
|
||||
"seeders": 2,
|
||||
"leechers": 1,
|
||||
"languages": [ { "id": 1, "name": "English" } ],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"fullSeason": true
|
||||
},
|
||||
{
|
||||
"guid": "4567",
|
||||
"protocol": "torrent",
|
||||
"age": 1,
|
||||
"title": "Test Release",
|
||||
"indexer": "kickass torrents",
|
||||
"indexerId": 2,
|
||||
"size": 1234,
|
||||
"rejected": true,
|
||||
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
|
||||
"seeders": 2,
|
||||
"leechers": 1,
|
||||
"languages": [ { "id": 1, "name": "English" } ],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
}
|
||||
]);
|
||||
let expected_filtered_sonarr_release = SonarrRelease {
|
||||
full_season: true,
|
||||
..release()
|
||||
};
|
||||
let expected_raw_sonarr_releases = vec![
|
||||
SonarrRelease {
|
||||
full_season: true,
|
||||
..release()
|
||||
},
|
||||
SonarrRelease {
|
||||
guid: "4567".to_owned(),
|
||||
..release()
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(release_json),
|
||||
None,
|
||||
SonarrEvent::GetSeasonReleases((1, 1)),
|
||||
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()]);
|
||||
app_arc.lock().await.data.sonarr_data.season_details_modal =
|
||||
Some(SeasonDetailsModal::default());
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Releases(releases_vec) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1)))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_releases
|
||||
.items,
|
||||
vec![expected_filtered_sonarr_release]
|
||||
);
|
||||
assert_eq!(releases_vec, expected_raw_sonarr_releases);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_season_releases_event_empty_season_details_modal() {
|
||||
let release_json = json!([
|
||||
{
|
||||
"guid": "1234",
|
||||
"protocol": "torrent",
|
||||
"age": 1,
|
||||
"title": "Test Release",
|
||||
"indexer": "kickass torrents",
|
||||
"indexerId": 2,
|
||||
"size": 1234,
|
||||
"rejected": true,
|
||||
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
|
||||
"seeders": 2,
|
||||
"leechers": 1,
|
||||
"languages": [ { "id": 1, "name": "English" } ],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
"fullSeason": true
|
||||
},
|
||||
{
|
||||
"guid": "4567",
|
||||
"protocol": "usenet",
|
||||
"age": 1,
|
||||
"title": "Test Release",
|
||||
"indexer": "kickass torrents",
|
||||
"indexerId": 2,
|
||||
"size": 1234,
|
||||
"rejected": true,
|
||||
"rejections": [ "Unknown quality profile", "Release is already mapped" ],
|
||||
"seeders": 2,
|
||||
"leechers": 1,
|
||||
"languages": [ { "id": 1, "name": "English" } ],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" }},
|
||||
}
|
||||
]);
|
||||
let expected_sonarr_release = SonarrRelease {
|
||||
full_season: true,
|
||||
..release()
|
||||
};
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(release_json),
|
||||
None,
|
||||
SonarrEvent::GetSeasonReleases((1, 1)),
|
||||
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()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::GetSeasonReleases((1, 1)))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.season_details_modal
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.season_releases
|
||||
.items,
|
||||
vec![expected_sonarr_release]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_season_history_event() {
|
||||
let history_json = json!([{
|
||||
"id": 123,
|
||||
"sourceTitle": "z episode",
|
||||
"episodeId": 1007,
|
||||
"quality": { "quality": { "name": "Bluray-1080p" } },
|
||||
"languages": [{ "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" } },
|
||||
"languages": [{ "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: Vec<SonarrHistoryItem> = 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((1, 1)),
|
||||
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;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::SonarrHistoryItems(history) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1)))
|
||||
.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!([{
|
||||
"id": 123,
|
||||
"sourceTitle": "z episode",
|
||||
"episodeId": 1007,
|
||||
"quality": { "quality": { "name": "Bluray-1080p" } },
|
||||
"languages": [{ "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" } },
|
||||
"languages": [{ "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: Vec<SonarrHistoryItem> = 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((1, 1)),
|
||||
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()]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::SonarrHistoryItems(history) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetSeasonHistory((1, 1)))
|
||||
.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_trigger_automatic_season_search_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "SeasonSearch",
|
||||
"seriesId": 1,
|
||||
"seasonNumber": 1
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((1, 1)),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::TriggerAutomaticSeasonSearch((1, 1)))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
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::models::Route;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info, warn};
|
||||
use serde_json::{json, Value};
|
||||
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<Value> {
|
||||
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::<AddSeriesBody, Value>(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::<Value, ()>(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::<Value>(&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::<Value, ()>(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<Series> {
|
||||
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<Vec<SonarrHistoryItem>> {
|
||||
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<SonarrHistoryItem>>(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<Vec<Series>> {
|
||||
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<Series>>(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<Vec<AddSeriesSearchResult>> {
|
||||
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<AddSeriesSearchResult>>(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<Value> {
|
||||
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::<SonarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn update_all_series(&mut self) -> Result<Value> {
|
||||
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::<SonarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn update_and_scan_series(
|
||||
&mut self,
|
||||
series_id: i64,
|
||||
) -> Result<Value> {
|
||||
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::<SonarrCommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::sonarr_models::SonarrReleaseDownloadBody;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_download_sonarr_release_event_uses_provided_params() {
|
||||
let params = SonarrReleaseDownloadBody {
|
||||
guid: "1234".to_owned(),
|
||||
indexer_id: 2,
|
||||
series_id: Some(1),
|
||||
..SonarrReleaseDownloadBody::default()
|
||||
};
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"guid": "1234",
|
||||
"indexerId": 2,
|
||||
"seriesId": 1,
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
SonarrEvent::DownloadRelease(params.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DownloadRelease(params))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{Network, NetworkEvent, NetworkResource};
|
||||
use crate::{
|
||||
models::{
|
||||
servarr_models::{AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag},
|
||||
sonarr_models::{
|
||||
AddSeriesBody, DeleteSeriesParams, EditSeriesParams, IndexerSettings,
|
||||
SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTaskName,
|
||||
},
|
||||
},
|
||||
network::RequestMethod,
|
||||
};
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_network_tests.rs"]
|
||||
mod sonarr_network_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_network_test_utils.rs"]
|
||||
mod sonarr_network_test_utils;
|
||||
|
||||
mod blocklist;
|
||||
mod downloads;
|
||||
mod history;
|
||||
mod indexers;
|
||||
mod library;
|
||||
mod root_folders;
|
||||
mod system;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum SonarrEvent {
|
||||
AddRootFolder(AddRootFolderBody),
|
||||
AddSeries(AddSeriesBody),
|
||||
AddTag(String),
|
||||
ClearBlocklist,
|
||||
DeleteBlocklistItem(i64),
|
||||
DeleteDownload(i64),
|
||||
DeleteEpisodeFile(i64),
|
||||
DeleteIndexer(i64),
|
||||
DeleteRootFolder(i64),
|
||||
DeleteSeries(DeleteSeriesParams),
|
||||
DeleteTag(i64),
|
||||
DownloadRelease(SonarrReleaseDownloadBody),
|
||||
EditAllIndexerSettings(IndexerSettings),
|
||||
EditIndexer(EditIndexerParams),
|
||||
EditSeries(EditSeriesParams),
|
||||
GetAllIndexerSettings,
|
||||
GetBlocklist,
|
||||
GetDownloads(u64),
|
||||
GetHistory(u64),
|
||||
GetHostConfig,
|
||||
GetIndexers,
|
||||
GetEpisodeDetails(i64),
|
||||
GetEpisodes(i64),
|
||||
GetEpisodeFiles(i64),
|
||||
GetEpisodeHistory(i64),
|
||||
GetLanguageProfiles,
|
||||
GetLogs(u64),
|
||||
GetDiskSpace,
|
||||
GetQualityProfiles,
|
||||
GetQueuedEvents,
|
||||
GetRootFolders,
|
||||
GetEpisodeReleases(i64),
|
||||
GetSeasonHistory((i64, i64)),
|
||||
GetSeasonReleases((i64, i64)),
|
||||
GetSecurityConfig,
|
||||
GetSeriesDetails(i64),
|
||||
GetSeriesHistory(i64),
|
||||
GetStatus,
|
||||
GetUpdates,
|
||||
GetTags,
|
||||
GetTasks,
|
||||
HealthCheck,
|
||||
ListSeries,
|
||||
MarkHistoryItemAsFailed(i64),
|
||||
SearchNewSeries(String),
|
||||
StartTask(SonarrTaskName),
|
||||
TestIndexer(i64),
|
||||
TestAllIndexers,
|
||||
ToggleSeasonMonitoring((i64, i64)),
|
||||
ToggleSeriesMonitoring(i64),
|
||||
ToggleEpisodeMonitoring(i64),
|
||||
TriggerAutomaticEpisodeSearch(i64),
|
||||
TriggerAutomaticSeasonSearch((i64, i64)),
|
||||
TriggerAutomaticSeriesSearch(i64),
|
||||
UpdateAllSeries,
|
||||
UpdateAndScanSeries(i64),
|
||||
UpdateDownloads,
|
||||
}
|
||||
|
||||
impl NetworkResource for SonarrEvent {
|
||||
fn resource(&self) -> &'static str {
|
||||
match &self {
|
||||
SonarrEvent::AddTag(_) | SonarrEvent::DeleteTag(_) | SonarrEvent::GetTags => "/tag",
|
||||
SonarrEvent::ClearBlocklist => "/blocklist/bulk",
|
||||
SonarrEvent::DownloadRelease(_) => "/release",
|
||||
SonarrEvent::DeleteBlocklistItem(_) => "/blocklist",
|
||||
SonarrEvent::GetAllIndexerSettings | SonarrEvent::EditAllIndexerSettings(_) => {
|
||||
"/config/indexer"
|
||||
}
|
||||
SonarrEvent::GetEpisodeFiles(_) | SonarrEvent::DeleteEpisodeFile(_) => "/episodefile",
|
||||
SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000",
|
||||
SonarrEvent::GetDownloads(_) | SonarrEvent::DeleteDownload(_) => "/queue",
|
||||
SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode",
|
||||
SonarrEvent::GetHistory(_) | SonarrEvent::GetEpisodeHistory(_) => "/history",
|
||||
SonarrEvent::GetHostConfig | SonarrEvent::GetSecurityConfig => "/config/host",
|
||||
SonarrEvent::GetIndexers | SonarrEvent::DeleteIndexer(_) | SonarrEvent::EditIndexer(_) => {
|
||||
"/indexer"
|
||||
}
|
||||
SonarrEvent::GetLanguageProfiles => "/language",
|
||||
SonarrEvent::GetLogs(_) => "/log",
|
||||
SonarrEvent::GetDiskSpace => "/diskspace",
|
||||
SonarrEvent::GetQualityProfiles => "/qualityprofile",
|
||||
SonarrEvent::GetQueuedEvents
|
||||
| SonarrEvent::StartTask(_)
|
||||
| SonarrEvent::TriggerAutomaticSeriesSearch(_)
|
||||
| SonarrEvent::TriggerAutomaticSeasonSearch(_)
|
||||
| SonarrEvent::TriggerAutomaticEpisodeSearch(_)
|
||||
| SonarrEvent::UpdateAllSeries
|
||||
| SonarrEvent::UpdateAndScanSeries(_)
|
||||
| SonarrEvent::UpdateDownloads => "/command",
|
||||
SonarrEvent::GetRootFolders
|
||||
| SonarrEvent::DeleteRootFolder(_)
|
||||
| SonarrEvent::AddRootFolder(_) => "/rootfolder",
|
||||
SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release",
|
||||
SonarrEvent::GetSeriesHistory(_) | SonarrEvent::GetSeasonHistory(_) => "/history/series",
|
||||
SonarrEvent::GetStatus => "/system/status",
|
||||
SonarrEvent::GetTasks => "/system/task",
|
||||
SonarrEvent::GetUpdates => "/update",
|
||||
SonarrEvent::HealthCheck => "/health",
|
||||
SonarrEvent::AddSeries(_)
|
||||
| SonarrEvent::ListSeries
|
||||
| SonarrEvent::GetSeriesDetails(_)
|
||||
| SonarrEvent::DeleteSeries(_)
|
||||
| SonarrEvent::EditSeries(_)
|
||||
| SonarrEvent::ToggleSeasonMonitoring(_)
|
||||
| SonarrEvent::ToggleSeriesMonitoring(_) => "/series",
|
||||
SonarrEvent::SearchNewSeries(_) => "/series/lookup",
|
||||
SonarrEvent::MarkHistoryItemAsFailed(_) => "/history/failed",
|
||||
SonarrEvent::TestIndexer(_) => "/indexer/test",
|
||||
SonarrEvent::TestAllIndexers => "/indexer/testall",
|
||||
SonarrEvent::ToggleEpisodeMonitoring(_) => "/episode/monitor",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SonarrEvent> for NetworkEvent {
|
||||
fn from(sonarr_event: SonarrEvent) -> Self {
|
||||
NetworkEvent::Sonarr(sonarr_event)
|
||||
}
|
||||
}
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub async fn handle_sonarr_event(
|
||||
&mut self,
|
||||
sonarr_event: SonarrEvent,
|
||||
) -> Result<SonarrSerdeable> {
|
||||
match sonarr_event {
|
||||
SonarrEvent::AddRootFolder(path) => self
|
||||
.add_sonarr_root_folder(path)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::AddSeries(body) => self
|
||||
.add_sonarr_series(body)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::AddTag(tag) => self.add_sonarr_tag(tag).await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::ClearBlocklist => self
|
||||
.clear_sonarr_blocklist()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetAllIndexerSettings => self
|
||||
.get_all_sonarr_indexer_settings()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteBlocklistItem(blocklist_item_id) => self
|
||||
.delete_sonarr_blocklist_item(blocklist_item_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteDownload(download_id) => self
|
||||
.delete_sonarr_download(download_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteEpisodeFile(episode_file_id) => self
|
||||
.delete_sonarr_episode_file(episode_file_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteIndexer(indexer_id) => self
|
||||
.delete_sonarr_indexer(indexer_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteRootFolder(root_folder_id) => self
|
||||
.delete_sonarr_root_folder(root_folder_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DeleteSeries(params) => {
|
||||
self.delete_series(params).await.map(SonarrSerdeable::from)
|
||||
}
|
||||
SonarrEvent::DeleteTag(tag_id) => self
|
||||
.delete_sonarr_tag(tag_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::DownloadRelease(sonarr_release_download_body) => self
|
||||
.download_sonarr_release(sonarr_release_download_body)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::EditAllIndexerSettings(params) => self
|
||||
.edit_all_sonarr_indexer_settings(params)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::EditIndexer(params) => self
|
||||
.edit_sonarr_indexer(params)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::EditSeries(params) => self
|
||||
.edit_sonarr_series(params)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetBlocklist => self.get_sonarr_blocklist().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetDownloads(count) => self
|
||||
.get_sonarr_downloads(count)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetEpisodes(series_id) => self
|
||||
.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
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetEpisodeHistory(episode_id) => self
|
||||
.get_sonarr_episode_history(episode_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetHistory(events) => self
|
||||
.get_sonarr_history(events)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetHostConfig => self
|
||||
.get_sonarr_host_config()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetIndexers => self.get_sonarr_indexers().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetLanguageProfiles => self
|
||||
.get_sonarr_language_profiles()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetLogs(events) => self
|
||||
.get_sonarr_logs(events)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetDiskSpace => self.get_sonarr_diskspace().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetQualityProfiles => self
|
||||
.get_sonarr_quality_profiles()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetQueuedEvents => self
|
||||
.get_queued_sonarr_events()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetRootFolders => self
|
||||
.get_sonarr_root_folders()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetEpisodeReleases(params) => self
|
||||
.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
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetSecurityConfig => self
|
||||
.get_sonarr_security_config()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetSeriesDetails(series_id) => self
|
||||
.get_series_details(series_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetSeriesHistory(series_id) => self
|
||||
.get_sonarr_series_history(series_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetTags => self.get_sonarr_tags().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetTasks => self.get_sonarr_tasks().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::GetUpdates => self.get_sonarr_updates().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::HealthCheck => self
|
||||
.get_sonarr_healthcheck()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::ListSeries => self.list_series().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::MarkHistoryItemAsFailed(history_item_id) => self
|
||||
.mark_sonarr_history_item_as_failed(history_item_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::SearchNewSeries(query) => self
|
||||
.search_sonarr_series(query)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::StartTask(task_name) => self
|
||||
.start_sonarr_task(task_name)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::TestIndexer(indexer_id) => self
|
||||
.test_sonarr_indexer(indexer_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::TestAllIndexers => self
|
||||
.test_all_sonarr_indexers()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::ToggleEpisodeMonitoring(episode_id) => self
|
||||
.toggle_sonarr_episode_monitoring(episode_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::ToggleSeasonMonitoring(params) => self
|
||||
.toggle_sonarr_season_monitoring(params)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::ToggleSeriesMonitoring(series_id) => self
|
||||
.toggle_sonarr_series_monitoring(series_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch(params) => self
|
||||
.trigger_automatic_season_search(params)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::TriggerAutomaticSeriesSearch(series_id) => self
|
||||
.trigger_automatic_series_search(series_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::TriggerAutomaticEpisodeSearch(episode_id) => self
|
||||
.trigger_automatic_episode_search(episode_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::UpdateAllSeries => self.update_all_series().await.map(SonarrSerdeable::from),
|
||||
SonarrEvent::UpdateAndScanSeries(series_id) => self
|
||||
.update_and_scan_series(series_id)
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
SonarrEvent::UpdateDownloads => self
|
||||
.update_sonarr_downloads()
|
||||
.await
|
||||
.map(SonarrSerdeable::from),
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_sonarr_tag(&mut self, tag: String) -> Result<Tag> {
|
||||
info!("Adding a new Sonarr tag");
|
||||
let event = SonarrEvent::AddTag(String::new());
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": tag })),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<Value, Tag>(request_props, |tag, mut app| {
|
||||
app.data.sonarr_data.tags_map.insert(tag.id, tag.label);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_sonarr_tag(&mut self, id: i64) -> Result<()> {
|
||||
info!("Deleting Sonarr tag with id: {id}");
|
||||
let event = SonarrEvent::DeleteTag(id);
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_sonarr_healthcheck(&mut self) -> Result<()> {
|
||||
info!("Performing Sonarr health check");
|
||||
let event = SonarrEvent::HealthCheck;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_sonarr_language_profiles(&mut self) -> Result<Vec<Language>> {
|
||||
info!("Fetching Sonarr language profiles");
|
||||
let event = SonarrEvent::GetLanguageProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Language>>(request_props, |language_profiles_vec, mut app| {
|
||||
app.data.sonarr_data.language_profiles_map = language_profiles_vec
|
||||
.into_iter()
|
||||
.map(|language| (language.id, language.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_sonarr_quality_profiles(&mut self) -> Result<Vec<QualityProfile>> {
|
||||
info!("Fetching Sonarr quality profiles");
|
||||
let event = SonarrEvent::GetQualityProfiles;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QualityProfile>>(request_props, |quality_profiles, mut app| {
|
||||
app.data.sonarr_data.quality_profile_map = quality_profiles
|
||||
.into_iter()
|
||||
.map(|profile| (profile.id, profile.name))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_sonarr_tags(&mut self) -> Result<Vec<Tag>> {
|
||||
info!("Fetching Sonarr tags");
|
||||
let event = SonarrEvent::GetTags;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Tag>>(request_props, |tags_vec, mut app| {
|
||||
app.data.sonarr_data.tags_map = tags_vec
|
||||
.into_iter()
|
||||
.map(|tag| (tag.id, tag.label))
|
||||
.collect();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn extract_and_add_sonarr_tag_ids_vec(
|
||||
&mut self,
|
||||
edit_tags: &str,
|
||||
) -> Vec<i64> {
|
||||
let missing_tags_vec = {
|
||||
let tags_map = &self.app.lock().await.data.sonarr_data.tags_map;
|
||||
edit_tags
|
||||
.split(',')
|
||||
.filter(|&tag| {
|
||||
!tag.is_empty() && tags_map.get_by_right(tag.to_lowercase().trim()).is_none()
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
};
|
||||
|
||||
for tag in missing_tags_vec {
|
||||
self
|
||||
.add_sonarr_tag(tag.trim().to_owned())
|
||||
.await
|
||||
.expect("Unable to add tag");
|
||||
}
|
||||
|
||||
let app = self.app.lock().await;
|
||||
edit_tags
|
||||
.split(',')
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.map(|tag| {
|
||||
*app
|
||||
.data
|
||||
.sonarr_data
|
||||
.tags_map
|
||||
.get_by_right(tag.to_lowercase().trim())
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use log::{debug, info};
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_root_folders_network_tests.rs"]
|
||||
mod sonarr_root_folders_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn add_sonarr_root_folder(
|
||||
&mut self,
|
||||
add_root_folder_body: AddRootFolderBody,
|
||||
) -> Result<Value> {
|
||||
info!("Adding new root folder to Sonarr");
|
||||
let event = SonarrEvent::AddRootFolder(AddRootFolderBody::default());
|
||||
|
||||
debug!("Add root folder body: {add_root_folder_body:?}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Post,
|
||||
Some(add_root_folder_body),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<AddRootFolderBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn delete_sonarr_root_folder(
|
||||
&mut self,
|
||||
root_folder_id: i64,
|
||||
) -> Result<()> {
|
||||
let event = SonarrEvent::DeleteRootFolder(root_folder_id);
|
||||
info!("Deleting Sonarr root folder for folder with id: {root_folder_id}");
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(
|
||||
event,
|
||||
RequestMethod::Delete,
|
||||
None::<()>,
|
||||
Some(format!("/{root_folder_id}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), ()>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_root_folders(
|
||||
&mut self,
|
||||
) -> Result<Vec<RootFolder>> {
|
||||
info!("Fetching Sonarr root folders");
|
||||
let event = SonarrEvent::GetRootFolders;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<RootFolder>>(request_props, |root_folders, mut app| {
|
||||
app.data.sonarr_data.root_folders.set_items(root_folders);
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_models::{AddRootFolderBody, RootFolder};
|
||||
use crate::models::sonarr_models::SonarrSerdeable;
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::sonarr_network_test_utils::test_utils::root_folder;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use pretty_assertions::assert_eq;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[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!({
|
||||
"path": "/nfs/test"
|
||||
})),
|
||||
Some(json!({})),
|
||||
None,
|
||||
SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::AddRootFolder(expected_add_root_folder_body))
|
||||
.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_delete_sonarr_root_folder_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::DeleteRootFolder(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DeleteRootFolder(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_root_folders_event() {
|
||||
let root_folder_json = json!([{
|
||||
"id": 1,
|
||||
"path": "/nfs",
|
||||
"accessible": true,
|
||||
"freeSpace": 219902325555200u64,
|
||||
}]);
|
||||
let response: Vec<RootFolder> = serde_json::from_value(root_folder_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(root_folder_json),
|
||||
None,
|
||||
SonarrEvent::GetRootFolders,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::RootFolders(root_folders) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetRootFolders)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.root_folders.items,
|
||||
vec![root_folder()]
|
||||
);
|
||||
assert_eq!(root_folders, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
#[cfg(test)]
|
||||
pub(in crate::network::sonarr_network) mod test_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};
|
||||
|
||||
pub const SERIES_JSON: &str = r#"{
|
||||
"title": "Test",
|
||||
"status": "continuing",
|
||||
"ended": false,
|
||||
"overview": "Blah blah blah",
|
||||
"network": "HBO",
|
||||
"seasons": [
|
||||
{
|
||||
"seasonNumber": 1,
|
||||
"monitored": true,
|
||||
"statistics": {
|
||||
"previousAiring": "2022-10-24T01:00:00Z",
|
||||
"episodeFileCount": 10,
|
||||
"episodeCount": 10,
|
||||
"totalEpisodeCount": 10,
|
||||
"sizeOnDisk": 36708563419,
|
||||
"percentOfEpisodes": 100.0
|
||||
}
|
||||
}
|
||||
],
|
||||
"year": 2022,
|
||||
"path": "/nfs/tv/Test",
|
||||
"qualityProfileId": 6,
|
||||
"languageProfileId": 1,
|
||||
"seasonFolder": true,
|
||||
"monitored": true,
|
||||
"runtime": 63,
|
||||
"tvdbId": 371572,
|
||||
"seriesType": "standard",
|
||||
"certification": "TV-MA",
|
||||
"genres": ["cool", "family", "fun"],
|
||||
"tags": [3],
|
||||
"ratings": {"votes": 406744, "value": 8.4},
|
||||
"statistics": {
|
||||
"seasonCount": 2,
|
||||
"episodeFileCount": 18,
|
||||
"episodeCount": 18,
|
||||
"totalEpisodeCount": 50,
|
||||
"sizeOnDisk": 63894022699,
|
||||
"percentOfEpisodes": 100.0
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const EPISODE_JSON: &str = r#"{
|
||||
"seriesId": 1,
|
||||
"tvdbId": 1234,
|
||||
"episodeFileId": 1,
|
||||
"seasonNumber": 1,
|
||||
"episodeNumber": 1,
|
||||
"title": "Something cool",
|
||||
"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,
|
||||
"dateAdded": "2024-02-10T07:28:45Z",
|
||||
"languages": [{ "id": 1, "name": "English" }],
|
||||
"quality": { "quality": { "name": "Bluray-1080p" } },
|
||||
"mediaInfo": {
|
||||
"audioBitrate": 0,
|
||||
"audioChannels": 7.1,
|
||||
"audioCodec": "AAC",
|
||||
"audioLanguages": "eng",
|
||||
"audioStreamCount": 1,
|
||||
"videoBitDepth": 10,
|
||||
"videoBitrate": 0,
|
||||
"videoCodec": "x265",
|
||||
"videoFps": 23.976,
|
||||
"resolution": "1920x1080",
|
||||
"runTime": "23:51",
|
||||
"scanType": "Progressive",
|
||||
"subtitles": "English"
|
||||
}
|
||||
},
|
||||
"hasFile": true,
|
||||
"monitored": true,
|
||||
"id": 1
|
||||
}"#;
|
||||
|
||||
pub 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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_series_search_result_statistics() -> AddSeriesSearchResultStatistics {
|
||||
AddSeriesSearchResultStatistics { season_count: 3 }
|
||||
}
|
||||
|
||||
pub 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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn download_record() -> DownloadRecord {
|
||||
DownloadRecord {
|
||||
title: "Test Download Title".to_owned(),
|
||||
status: DownloadStatus::Downloading,
|
||||
id: 1,
|
||||
episode_id: Some(Number::from(1i64)),
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downloads_response() -> DownloadsResponse {
|
||||
DownloadsResponse {
|
||||
records: vec![download_record()],
|
||||
}
|
||||
}
|
||||
|
||||
pub 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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn genres() -> Vec<String> {
|
||||
vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()]
|
||||
}
|
||||
|
||||
pub 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()
|
||||
}
|
||||
}
|
||||
|
||||
pub 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(),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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")),
|
||||
},
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indexer_settings() -> IndexerSettings {
|
||||
IndexerSettings {
|
||||
id: 1,
|
||||
minimum_age: 1,
|
||||
retention: 1,
|
||||
maximum_size: 12345,
|
||||
rss_sync_interval: 60,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language() -> Language {
|
||||
Language {
|
||||
id: 1,
|
||||
name: "English".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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: Some("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()),
|
||||
}
|
||||
}
|
||||
pub fn quality() -> Quality {
|
||||
Quality {
|
||||
name: "Bluray-1080p".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quality_wrapper() -> QualityWrapper {
|
||||
QualityWrapper { quality: quality() }
|
||||
}
|
||||
|
||||
pub fn rating() -> Rating {
|
||||
Rating {
|
||||
votes: 406744,
|
||||
value: 8.4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn season() -> Season {
|
||||
Season {
|
||||
title: None,
|
||||
season_number: 1,
|
||||
monitored: true,
|
||||
statistics: Some(season_statistics()),
|
||||
}
|
||||
}
|
||||
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rejections() -> Vec<String> {
|
||||
vec![
|
||||
"Unknown quality profile".to_owned(),
|
||||
"Release is already mapped".to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
pub 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_folder() -> RootFolder {
|
||||
RootFolder {
|
||||
id: 1,
|
||||
path: "/nfs".to_owned(),
|
||||
accessible: true,
|
||||
free_space: 219902325555200,
|
||||
unmapped_folders: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::app::App;
|
||||
use crate::models::servarr_data::sonarr::modals::AddSeriesModal;
|
||||
use crate::models::servarr_models::{
|
||||
AddRootFolderBody, EditIndexerParams, Language, QualityProfile, Tag,
|
||||
};
|
||||
use crate::models::sonarr_models::{
|
||||
AddSeriesBody, EditSeriesParams, IndexerSettings, SonarrTaskName,
|
||||
};
|
||||
use crate::models::sonarr_models::{DeleteSeriesParams, SonarrSerdeable};
|
||||
use crate::network::{
|
||||
network_tests::test_utils::mock_servarr_api, sonarr_network::SonarrEvent, Network,
|
||||
NetworkEvent, NetworkResource, RequestMethod,
|
||||
};
|
||||
use bimap::BiMap;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use reqwest::Client;
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_all_indexer_settings(
|
||||
#[values(
|
||||
SonarrEvent::GetAllIndexerSettings,
|
||||
SonarrEvent::EditAllIndexerSettings(IndexerSettings::default())
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/config/indexer");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_episode(
|
||||
#[values(SonarrEvent::GetEpisodes(0), SonarrEvent::GetEpisodeDetails(0))] event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/episode");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_series(
|
||||
#[values(
|
||||
SonarrEvent::AddSeries(AddSeriesBody::default()),
|
||||
SonarrEvent::ListSeries,
|
||||
SonarrEvent::GetSeriesDetails(0),
|
||||
SonarrEvent::DeleteSeries(DeleteSeriesParams::default()),
|
||||
SonarrEvent::EditSeries(EditSeriesParams::default()),
|
||||
SonarrEvent::ToggleSeasonMonitoring((0, 0)),
|
||||
SonarrEvent::ToggleSeriesMonitoring(0),
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/series");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_tag(
|
||||
#[values(
|
||||
SonarrEvent::AddTag(String::new()),
|
||||
SonarrEvent::DeleteTag(0),
|
||||
SonarrEvent::GetTags
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/tag");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_host_config(
|
||||
#[values(SonarrEvent::GetHostConfig, SonarrEvent::GetSecurityConfig)] event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/config/host");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_command(
|
||||
#[values(
|
||||
SonarrEvent::GetQueuedEvents,
|
||||
SonarrEvent::StartTask(SonarrTaskName::default()),
|
||||
SonarrEvent::TriggerAutomaticEpisodeSearch(0),
|
||||
SonarrEvent::TriggerAutomaticSeasonSearch((0, 0)),
|
||||
SonarrEvent::TriggerAutomaticSeriesSearch(0),
|
||||
SonarrEvent::UpdateAllSeries,
|
||||
SonarrEvent::UpdateAndScanSeries(0),
|
||||
SonarrEvent::UpdateDownloads
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/command");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_indexer(
|
||||
#[values(
|
||||
SonarrEvent::GetIndexers,
|
||||
SonarrEvent::DeleteIndexer(0),
|
||||
SonarrEvent::EditIndexer(EditIndexerParams::default())
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/indexer");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_history(
|
||||
#[values(SonarrEvent::GetHistory(0), SonarrEvent::GetEpisodeHistory(0))] event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/history");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_series_history(
|
||||
#[values(
|
||||
SonarrEvent::GetSeriesHistory(0),
|
||||
SonarrEvent::GetSeasonHistory((0, 0))
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/history/series");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_queue(
|
||||
#[values(SonarrEvent::GetDownloads(0), SonarrEvent::DeleteDownload(0))] event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/queue");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_root_folder(
|
||||
#[values(
|
||||
SonarrEvent::GetRootFolders,
|
||||
SonarrEvent::DeleteRootFolder(0),
|
||||
SonarrEvent::AddRootFolder(AddRootFolderBody::default())
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/rootfolder");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_release(
|
||||
#[values(
|
||||
SonarrEvent::GetSeasonReleases((0, 0)),
|
||||
SonarrEvent::GetEpisodeReleases(0)
|
||||
)]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/release");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_resource_episode_file(
|
||||
#[values(SonarrEvent::GetEpisodeFiles(0), SonarrEvent::DeleteEpisodeFile(0))]
|
||||
event: SonarrEvent,
|
||||
) {
|
||||
assert_str_eq!(event.resource(), "/episodefile");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")]
|
||||
#[case(SonarrEvent::DeleteBlocklistItem(0), "/blocklist")]
|
||||
#[case(SonarrEvent::HealthCheck, "/health")]
|
||||
#[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")]
|
||||
#[case(SonarrEvent::GetDiskSpace, "/diskspace")]
|
||||
#[case(SonarrEvent::GetLanguageProfiles, "/language")]
|
||||
#[case(SonarrEvent::GetLogs(500), "/log")]
|
||||
#[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")]
|
||||
#[case(SonarrEvent::GetStatus, "/system/status")]
|
||||
#[case(SonarrEvent::GetTasks, "/system/task")]
|
||||
#[case(SonarrEvent::GetUpdates, "/update")]
|
||||
#[case(SonarrEvent::MarkHistoryItemAsFailed(0), "/history/failed")]
|
||||
#[case(SonarrEvent::SearchNewSeries(String::new()), "/series/lookup")]
|
||||
#[case(SonarrEvent::TestIndexer(0), "/indexer/test")]
|
||||
#[case(SonarrEvent::TestAllIndexers, "/indexer/testall")]
|
||||
#[case(SonarrEvent::ToggleEpisodeMonitoring(0), "/episode/monitor")]
|
||||
fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) {
|
||||
assert_str_eq!(event.resource(), expected_uri);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_sonarr_event() {
|
||||
assert_eq!(
|
||||
NetworkEvent::Sonarr(SonarrEvent::HealthCheck),
|
||||
NetworkEvent::from(SonarrEvent::HealthCheck)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_add_sonarr_tag() {
|
||||
let tag_json = json!({ "id": 3, "label": "testing" });
|
||||
let response: Tag = serde_json::from_value(tag_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": "testing" })),
|
||||
Some(tag_json),
|
||||
None,
|
||||
SonarrEvent::AddTag(String::new()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.data.sonarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Tag(tag) = network
|
||||
.handle_sonarr_event(SonarrEvent::AddTag("testing".to_owned()))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.tags_map,
|
||||
BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "testing".to_owned())
|
||||
])
|
||||
);
|
||||
assert_eq!(tag, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_delete_sonarr_tag_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Delete,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::DeleteTag(1),
|
||||
Some("/1"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert!(network
|
||||
.handle_sonarr_event(SonarrEvent::DeleteTag(1))
|
||||
.await
|
||||
.is_ok());
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_healthcheck_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
SonarrEvent::HealthCheck,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
let _ = network.handle_sonarr_event(SonarrEvent::HealthCheck).await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_language_profiles_event() {
|
||||
let language_profiles_json = json!([{
|
||||
"id": 2222,
|
||||
"name": "English"
|
||||
}]);
|
||||
let response: Vec<Language> = serde_json::from_value(language_profiles_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(language_profiles_json),
|
||||
None,
|
||||
SonarrEvent::GetLanguageProfiles,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::LanguageProfiles(language_profiles) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetLanguageProfiles)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.language_profiles_map,
|
||||
BiMap::from_iter([(2222i64, "English".to_owned())])
|
||||
);
|
||||
assert_eq!(language_profiles, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_quality_profiles_event() {
|
||||
let quality_profile_json = json!([{
|
||||
"id": 2222,
|
||||
"name": "HD - 1080p"
|
||||
}]);
|
||||
let response: Vec<QualityProfile> =
|
||||
serde_json::from_value(quality_profile_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(quality_profile_json),
|
||||
None,
|
||||
SonarrEvent::GetQualityProfiles,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::QualityProfiles(quality_profiles) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetQualityProfiles)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.quality_profile_map,
|
||||
BiMap::from_iter([(2222i64, "HD - 1080p".to_owned())])
|
||||
);
|
||||
assert_eq!(quality_profiles, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_tags_event() {
|
||||
let tags_json = json!([{
|
||||
"id": 2222,
|
||||
"label": "usenet"
|
||||
}]);
|
||||
let response: Vec<Tag> = serde_json::from_value(tags_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(tags_json),
|
||||
None,
|
||||
SonarrEvent::GetTags,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Tags(tags) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetTags)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.tags_map,
|
||||
BiMap::from_iter([(2222i64, "usenet".to_owned())])
|
||||
);
|
||||
assert_eq!(tags, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_and_add_sonarr_tag_ids_vec() {
|
||||
let app_arc = Arc::new(Mutex::new(App::test_default()));
|
||||
let tags = " test,HI ,, usenet ";
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.data.sonarr_data.tags_map = BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "hi".to_owned()),
|
||||
]);
|
||||
}
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
assert_eq!(
|
||||
network.extract_and_add_sonarr_tag_ids_vec(tags).await,
|
||||
vec![2, 3, 1]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_extract_and_add_sonarr_tag_ids_vec_add_missing_tags_first() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({ "label": "TESTING" })),
|
||||
Some(json!({ "id": 3, "label": "testing" })),
|
||||
None,
|
||||
SonarrEvent::GetTags,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let tags = "usenet, test, TESTING";
|
||||
{
|
||||
let mut app = app_arc.lock().await;
|
||||
app.data.sonarr_data.add_series_modal = Some(AddSeriesModal {
|
||||
tags: tags.into(),
|
||||
..AddSeriesModal::default()
|
||||
});
|
||||
app.data.sonarr_data.tags_map =
|
||||
BiMap::from_iter([(1, "usenet".to_owned()), (2, "test".to_owned())]);
|
||||
}
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
let tag_ids_vec = network.extract_and_add_sonarr_tag_ids_vec(tags).await;
|
||||
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(tag_ids_vec, vec![1, 2, 3]);
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.tags_map,
|
||||
BiMap::from_iter([
|
||||
(1, "usenet".to_owned()),
|
||||
(2, "test".to_owned()),
|
||||
(3, "testing".to_owned())
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
use crate::models::servarr_models::{
|
||||
CommandBody, DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
|
||||
};
|
||||
use crate::models::sonarr_models::{SonarrTask, SonarrTaskName, SystemStatus};
|
||||
use crate::models::{HorizontallyScrollableText, Scrollable, ScrollableText};
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use anyhow::Result;
|
||||
use indoc::formatdoc;
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sonarr_system_network_tests.rs"]
|
||||
mod sonarr_system_network_tests;
|
||||
|
||||
impl Network<'_, '_> {
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_host_config(
|
||||
&mut self,
|
||||
) -> Result<HostConfig> {
|
||||
info!("Fetching Sonarr host config");
|
||||
let event = SonarrEvent::GetHostConfig;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), HostConfig>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_logs(
|
||||
&mut self,
|
||||
events: u64,
|
||||
) -> Result<LogResponse> {
|
||||
info!("Fetching Sonarr logs");
|
||||
let event = SonarrEvent::GetLogs(events);
|
||||
|
||||
let params = format!("pageSize={events}&sortDirection=descending&sortKey=time");
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params))
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), LogResponse>(request_props, |log_response, mut app| {
|
||||
let mut logs = log_response.records;
|
||||
logs.reverse();
|
||||
|
||||
let log_lines = logs
|
||||
.into_iter()
|
||||
.map(|log| {
|
||||
if log.exception.is_some() {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log.logger.as_ref().unwrap(),
|
||||
log.exception_type.as_ref().unwrap(),
|
||||
log.exception.as_ref().unwrap()
|
||||
))
|
||||
} else {
|
||||
HorizontallyScrollableText::from(format!(
|
||||
"{}|{}|{}|{}",
|
||||
log.time,
|
||||
log.level.to_uppercase(),
|
||||
log.logger.as_ref().unwrap(),
|
||||
log.message.as_ref().unwrap()
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
app.data.sonarr_data.logs.set_items(log_lines);
|
||||
app.data.sonarr_data.logs.scroll_to_bottom();
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_diskspace(
|
||||
&mut self,
|
||||
) -> Result<Vec<DiskSpace>> {
|
||||
info!("Fetching Sonarr disk space");
|
||||
let event = SonarrEvent::GetDiskSpace;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<DiskSpace>>(request_props, |disk_space_vec, mut app| {
|
||||
app.data.sonarr_data.disk_space_vec = disk_space_vec;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_queued_sonarr_events(
|
||||
&mut self,
|
||||
) -> Result<Vec<QueueEvent>> {
|
||||
info!("Fetching Sonarr queued events");
|
||||
let event = SonarrEvent::GetQueuedEvents;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<QueueEvent>>(request_props, |queued_events_vec, mut app| {
|
||||
app
|
||||
.data
|
||||
.sonarr_data
|
||||
.queued_events
|
||||
.set_items(queued_events_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_security_config(
|
||||
&mut self,
|
||||
) -> Result<SecurityConfig> {
|
||||
info!("Fetching Sonarr security config");
|
||||
let event = SonarrEvent::GetSecurityConfig;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), SecurityConfig>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_status(
|
||||
&mut self,
|
||||
) -> Result<SystemStatus> {
|
||||
info!("Fetching Sonarr system status");
|
||||
let event = SonarrEvent::GetStatus;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), SystemStatus>(request_props, |system_status, mut app| {
|
||||
app.data.sonarr_data.version = system_status.version;
|
||||
app.data.sonarr_data.start_time = system_status.start_time;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_tasks(
|
||||
&mut self,
|
||||
) -> Result<Vec<SonarrTask>> {
|
||||
info!("Fetching Sonarr tasks");
|
||||
let event = SonarrEvent::GetTasks;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<SonarrTask>>(request_props, |tasks_vec, mut app| {
|
||||
app.data.sonarr_data.tasks.set_items(tasks_vec);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn get_sonarr_updates(
|
||||
&mut self,
|
||||
) -> Result<Vec<Update>> {
|
||||
info!("Fetching Sonarr updates");
|
||||
let event = SonarrEvent::GetUpdates;
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Get, None::<()>, None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<(), Vec<Update>>(request_props, |updates_vec, mut app| {
|
||||
let latest_installed = if updates_vec
|
||||
.iter()
|
||||
.any(|update| update.latest && update.installed_on.is_some())
|
||||
{
|
||||
"already".to_owned()
|
||||
} else {
|
||||
"not".to_owned()
|
||||
};
|
||||
let updates = updates_vec
|
||||
.into_iter()
|
||||
.map(|update| {
|
||||
let install_status = if update.installed_on.is_some() {
|
||||
if update.installed {
|
||||
"(Currently Installed)".to_owned()
|
||||
} else {
|
||||
"(Previously Installed)".to_owned()
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let vec_to_bullet_points = |vec: Vec<String>| {
|
||||
vec
|
||||
.iter()
|
||||
.map(|change| format!(" * {change}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
let mut update_info = formatdoc!(
|
||||
"{} - {} {install_status}
|
||||
{}",
|
||||
update.version,
|
||||
update.release_date,
|
||||
"-".repeat(200)
|
||||
);
|
||||
|
||||
if let Some(new_changes) = update.changes.new {
|
||||
let changes = vec_to_bullet_points(new_changes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
New:
|
||||
{changes}"
|
||||
)
|
||||
}
|
||||
|
||||
if let Some(fixes) = update.changes.fixed {
|
||||
let fixes = vec_to_bullet_points(fixes);
|
||||
update_info = formatdoc!(
|
||||
"{update_info}
|
||||
Fixed:
|
||||
{fixes}"
|
||||
);
|
||||
}
|
||||
|
||||
update_info
|
||||
})
|
||||
.reduce(|version_1, version_2| format!("{version_1}\n\n\n{version_2}"))
|
||||
.unwrap();
|
||||
|
||||
app.data.sonarr_data.updates = ScrollableText::with_string(formatdoc!(
|
||||
"The latest version of Sonarr is {latest_installed} installed
|
||||
|
||||
{updates}"
|
||||
));
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(in crate::network::sonarr_network) async fn start_sonarr_task(
|
||||
&mut self,
|
||||
task: SonarrTaskName,
|
||||
) -> Result<Value> {
|
||||
let event = SonarrEvent::StartTask(task);
|
||||
let task_name = task.to_string();
|
||||
info!("Starting Sonarr task: {task_name}");
|
||||
|
||||
let body = CommandBody { name: task_name };
|
||||
|
||||
let request_props = self
|
||||
.request_props_from(event, RequestMethod::Post, Some(body), None, None)
|
||||
.await;
|
||||
|
||||
self
|
||||
.handle_request::<CommandBody, Value>(request_props, |_, _| ())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::servarr_models::{
|
||||
DiskSpace, HostConfig, LogResponse, QueueEvent, SecurityConfig, Update,
|
||||
};
|
||||
use crate::models::sonarr_models::{SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus};
|
||||
use crate::models::{HorizontallyScrollableText, ScrollableText};
|
||||
use crate::network::network_tests::test_utils::mock_servarr_api;
|
||||
use crate::network::sonarr_network::SonarrEvent;
|
||||
use crate::network::{Network, RequestMethod};
|
||||
use chrono::DateTime;
|
||||
use indoc::formatdoc;
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_host_config_event() {
|
||||
let host_config_response = json!({
|
||||
"bindAddress": "*",
|
||||
"port": 7878,
|
||||
"urlBase": "some.test.site/sonarr",
|
||||
"instanceName": "Sonarr",
|
||||
"applicationUrl": "https://some.test.site:7878/sonarr",
|
||||
"enableSsl": true,
|
||||
"sslPort": 9898,
|
||||
"sslCertPath": "/app/sonarr.pfx",
|
||||
"sslCertPassword": "test"
|
||||
});
|
||||
let response: HostConfig = serde_json::from_value(host_config_response.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(host_config_response),
|
||||
None,
|
||||
SonarrEvent::GetHostConfig,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::HostConfig(host_config) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetHostConfig)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(host_config, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_logs_event() {
|
||||
let expected_logs = vec![
|
||||
HorizontallyScrollableText::from(
|
||||
"2023-05-20 21:29:16 UTC|FATAL|SonarrError|Some.Big.Bad.Exception|test exception",
|
||||
),
|
||||
HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"),
|
||||
];
|
||||
let logs_response_json = json!({
|
||||
"page": 1,
|
||||
"pageSize": 500,
|
||||
"sortKey": "time",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 2,
|
||||
"records": [
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "info",
|
||||
"logger": "TestLogger",
|
||||
"message": "test message",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"time": "2023-05-20T21:29:16Z",
|
||||
"level": "fatal",
|
||||
"logger": "SonarrError",
|
||||
"exception": "test exception",
|
||||
"exceptionType": "Some.Big.Bad.Exception",
|
||||
"id": 2
|
||||
}
|
||||
]
|
||||
});
|
||||
let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(logs_response_json),
|
||||
None,
|
||||
SonarrEvent::GetLogs(500),
|
||||
None,
|
||||
Some("pageSize=500&sortDirection=descending&sortKey=time"),
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::LogResponse(logs) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetLogs(500))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.logs.items,
|
||||
expected_logs
|
||||
);
|
||||
assert!(app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.logs
|
||||
.current_selection()
|
||||
.text
|
||||
.contains("INFO"));
|
||||
assert_eq!(logs, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_diskspace_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(json!([
|
||||
{
|
||||
"freeSpace": 1111,
|
||||
"totalSpace": 2222,
|
||||
},
|
||||
{
|
||||
"freeSpace": 3333,
|
||||
"totalSpace": 4444
|
||||
}
|
||||
])),
|
||||
None,
|
||||
SonarrEvent::GetDiskSpace,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
let disk_space_vec = vec![
|
||||
DiskSpace {
|
||||
free_space: 1111,
|
||||
total_space: 2222,
|
||||
},
|
||||
DiskSpace {
|
||||
free_space: 3333,
|
||||
total_space: 4444,
|
||||
},
|
||||
];
|
||||
|
||||
if let SonarrSerdeable::DiskSpaces(disk_space) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetDiskSpace)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.disk_space_vec,
|
||||
disk_space_vec
|
||||
);
|
||||
assert_eq!(disk_space, disk_space_vec);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_queued_sonarr_events_event() {
|
||||
let queued_events_json = json!([{
|
||||
"name": "RefreshMonitoredDownloads",
|
||||
"commandName": "Refresh Monitored Downloads",
|
||||
"status": "completed",
|
||||
"queued": "2023-05-20T21:29:16Z",
|
||||
"started": "2023-05-20T21:29:16Z",
|
||||
"ended": "2023-05-20T21:29:16Z",
|
||||
"duration": "00:00:00.5111547",
|
||||
"trigger": "scheduled",
|
||||
}]);
|
||||
let response: Vec<QueueEvent> = serde_json::from_value(queued_events_json.clone()).unwrap();
|
||||
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
|
||||
let expected_event = QueueEvent {
|
||||
name: "RefreshMonitoredDownloads".to_owned(),
|
||||
command_name: "Refresh Monitored Downloads".to_owned(),
|
||||
status: "completed".to_owned(),
|
||||
queued: timestamp,
|
||||
started: Some(timestamp),
|
||||
ended: Some(timestamp),
|
||||
duration: Some("00:00:00.5111547".to_owned()),
|
||||
trigger: "scheduled".to_owned(),
|
||||
};
|
||||
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(queued_events_json),
|
||||
None,
|
||||
SonarrEvent::GetQueuedEvents,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::QueueEvents(events) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetQueuedEvents)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.queued_events.items,
|
||||
vec![expected_event]
|
||||
);
|
||||
assert_eq!(events, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_security_config_event() {
|
||||
let security_config_response = json!({
|
||||
"authenticationMethod": "forms",
|
||||
"authenticationRequired": "disabledForLocalAddresses",
|
||||
"username": "test",
|
||||
"password": "some password",
|
||||
"apiKey": "someApiKey12345",
|
||||
"certificateValidation": "disabledForLocalAddresses",
|
||||
});
|
||||
let response: SecurityConfig =
|
||||
serde_json::from_value(security_config_response.clone()).unwrap();
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(security_config_response),
|
||||
None,
|
||||
SonarrEvent::GetSecurityConfig,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::SecurityConfig(security_config) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetSecurityConfig)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(security_config, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_status_event() {
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(json!({
|
||||
"version": "v1",
|
||||
"startTime": "2023-02-25T20:16:43Z"
|
||||
})),
|
||||
None,
|
||||
SonarrEvent::GetStatus,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
let date_time = DateTime::from(DateTime::parse_from_rfc3339("2023-02-25T20:16:43Z").unwrap());
|
||||
|
||||
if let SonarrSerdeable::SystemStatus(status) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetStatus)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_str_eq!(app_arc.lock().await.data.sonarr_data.version, "v1");
|
||||
assert_eq!(app_arc.lock().await.data.sonarr_data.start_time, date_time);
|
||||
assert_eq!(
|
||||
status,
|
||||
SystemStatus {
|
||||
version: "v1".to_owned(),
|
||||
start_time: date_time
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_tasks_event() {
|
||||
let tasks_json = json!([{
|
||||
"name": "Application Update Check",
|
||||
"taskName": "ApplicationUpdateCheck",
|
||||
"interval": 360,
|
||||
"lastExecution": "2023-05-20T21:29:16Z",
|
||||
"nextExecution": "2023-05-20T21:29:16Z",
|
||||
},
|
||||
{
|
||||
"name": "Backup",
|
||||
"taskName": "Backup",
|
||||
"interval": 10080,
|
||||
"lastExecution": "2023-05-20T21:29:16Z",
|
||||
"nextExecution": "2023-05-20T21:29:16Z",
|
||||
}]);
|
||||
let response: Vec<SonarrTask> = serde_json::from_value(tasks_json.clone()).unwrap();
|
||||
let timestamp = DateTime::from(DateTime::parse_from_rfc3339("2023-05-20T21:29:16Z").unwrap());
|
||||
let expected_tasks = vec![
|
||||
SonarrTask {
|
||||
name: "Application Update Check".to_owned(),
|
||||
task_name: SonarrTaskName::ApplicationUpdateCheck,
|
||||
interval: 360,
|
||||
last_execution: timestamp,
|
||||
next_execution: timestamp,
|
||||
},
|
||||
SonarrTask {
|
||||
name: "Backup".to_owned(),
|
||||
task_name: SonarrTaskName::Backup,
|
||||
interval: 10080,
|
||||
last_execution: timestamp,
|
||||
next_execution: timestamp,
|
||||
},
|
||||
];
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(tasks_json),
|
||||
None,
|
||||
SonarrEvent::GetTasks,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Tasks(tasks) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetTasks)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.tasks.items,
|
||||
expected_tasks
|
||||
);
|
||||
assert_eq!(tasks, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_get_sonarr_updates_event() {
|
||||
let updates_json = json!([{
|
||||
"version": "4.3.2.1",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": true,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": true,
|
||||
"changes": {
|
||||
"new": [
|
||||
"Cool new thing"
|
||||
],
|
||||
"fixed": [
|
||||
"Some bugs killed"
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"version": "3.2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"installedOn": "2023-04-15T02:02:53Z",
|
||||
"latest": false,
|
||||
"changes": {
|
||||
"new": [
|
||||
"Cool new thing (old)",
|
||||
"Other cool new thing (old)"
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"version": "2.1.0",
|
||||
"releaseDate": "2023-04-15T02:02:53Z",
|
||||
"installed": false,
|
||||
"latest": false,
|
||||
"changes": {
|
||||
"fixed": [
|
||||
"Killed bug 1",
|
||||
"Fixed bug 2"
|
||||
]
|
||||
},
|
||||
}]);
|
||||
let response: Vec<Update> = serde_json::from_value(updates_json.clone()).unwrap();
|
||||
let line_break = "-".repeat(200);
|
||||
let expected_text = ScrollableText::with_string(formatdoc!(
|
||||
"
|
||||
The latest version of Sonarr is already installed
|
||||
|
||||
4.3.2.1 - 2023-04-15 02:02:53 UTC (Currently Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing
|
||||
Fixed:
|
||||
* Some bugs killed
|
||||
|
||||
|
||||
3.2.1.0 - 2023-04-15 02:02:53 UTC (Previously Installed)
|
||||
{line_break}
|
||||
New:
|
||||
* Cool new thing (old)
|
||||
* Other cool new thing (old)
|
||||
|
||||
|
||||
2.1.0 - 2023-04-15 02:02:53 UTC
|
||||
{line_break}
|
||||
Fixed:
|
||||
* Killed bug 1
|
||||
* Fixed bug 2"
|
||||
));
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Get,
|
||||
None,
|
||||
Some(updates_json),
|
||||
None,
|
||||
SonarrEvent::GetUpdates,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Updates(updates) = network
|
||||
.handle_sonarr_event(SonarrEvent::GetUpdates)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_str_eq!(
|
||||
app_arc.lock().await.data.sonarr_data.updates.get_text(),
|
||||
expected_text.get_text()
|
||||
);
|
||||
assert_eq!(updates, response);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handle_start_sonarr_task_event() {
|
||||
let response = json!({ "test": "test"});
|
||||
let (async_server, app_arc, _server) = mock_servarr_api(
|
||||
RequestMethod::Post,
|
||||
Some(json!({
|
||||
"name": "ApplicationUpdateCheck"
|
||||
})),
|
||||
Some(response.clone()),
|
||||
None,
|
||||
SonarrEvent::StartTask(SonarrTaskName::ApplicationUpdateCheck),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
app_arc
|
||||
.lock()
|
||||
.await
|
||||
.data
|
||||
.sonarr_data
|
||||
.tasks
|
||||
.set_items(vec![SonarrTask {
|
||||
task_name: SonarrTaskName::default(),
|
||||
..SonarrTask::default()
|
||||
}]);
|
||||
app_arc.lock().await.server_tabs.next();
|
||||
let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new());
|
||||
|
||||
if let SonarrSerdeable::Value(value) = network
|
||||
.handle_sonarr_event(SonarrEvent::StartTask(
|
||||
SonarrTaskName::ApplicationUpdateCheck,
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
async_server.assert_async().await;
|
||||
assert_eq!(value, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user