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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user